From 2d63817d99094183e3774c52915d8462c9e94c70 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 17 Jul 2020 17:03:05 +0200 Subject: [PATCH 01/62] Fix issue with FormikMarkdownTextField. The component got unusable if it was in preview mode and the value got resetted to ''. Now, the button component stays clickable if the user is in preview mode, regardless of the value of the input. --- .../src/components/forms/components/FormikMarkdownTextfield.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/forms/components/FormikMarkdownTextfield.tsx b/client/src/components/forms/components/FormikMarkdownTextfield.tsx index a2b0a2685..746182248 100644 --- a/client/src/components/forms/components/FormikMarkdownTextfield.tsx +++ b/client/src/components/forms/components/FormikMarkdownTextfield.tsx @@ -68,7 +68,7 @@ function FormikMarkdownTextfield({ name, className, ...other }: FormikTextFieldP className={classes.button} onClick={() => setPreview(!isPreview)} color={isPreview ? 'secondary' : 'default'} - disabled={!value} + disabled={!value && !isPreview} /> {isPreview ? ( From fac65068a649a6d9eb17258106d6be74469d813d Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 17 Jul 2020 22:26:29 +0200 Subject: [PATCH 02/62] Change FormikMarkdownTextfield to exit preview if value is not present. --- .../forms/components/FormikMarkdownTextfield.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/components/forms/components/FormikMarkdownTextfield.tsx b/client/src/components/forms/components/FormikMarkdownTextfield.tsx index 746182248..3c8feea16 100644 --- a/client/src/components/forms/components/FormikMarkdownTextfield.tsx +++ b/client/src/components/forms/components/FormikMarkdownTextfield.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import { useField, useFormikContext } from 'formik'; import 'github-markdown-css/github-markdown.css'; import { FileFind as PreviewIcon } from 'mdi-material-ui'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import AnimatedButton from '../../AnimatedButton'; import Markdown from '../../Markdown'; import FormikTextField, { FormikTextFieldProps } from './FormikTextField'; @@ -48,6 +48,12 @@ function FormikMarkdownTextfield({ name, className, ...other }: FormikTextFieldP const [isPreview, setPreview] = useState(false); + useEffect(() => { + if (!value) { + setPreview(false); + } + }, [value]); + const handleKeyDown: React.KeyboardEventHandler = (event) => { // FIXME: Does this need to be in here? Can it use the useKeyboardShortcut() hook or better - can this be handled by the parent component? if (event.ctrlKey && event.key === 'Enter') { From 57e4829bb534abd43478487fe5f1901737f30f15 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 17 Jul 2020 22:34:17 +0200 Subject: [PATCH 03/62] Change styles in the Logger. Message get a colroed 'label' prefix instead of getting colored as a whole. --- client/src/util/Logger.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/client/src/util/Logger.ts b/client/src/util/Logger.ts index 88c3a6432..410dede70 100644 --- a/client/src/util/Logger.ts +++ b/client/src/util/Logger.ts @@ -50,8 +50,9 @@ export class Logger { const context = options.context ?? this.context ?? 'Logger'; this.console.log( - `%c${timestamp} - [${context}] ${message}`, - `color: ${this.getColorForLevel(options.level)}` + `%c${this.getPrefixForLevel(options.level)}%c ${timestamp} - [${context}] ${message}`, + this.getPrefixCSS(options.level), + '' ); } @@ -95,12 +96,34 @@ export class Logger { this.log(message, { ...options, level: LogLevel.ERROR }); } + private getPrefixCSS(level: LogLevel): string { + const bgColor = this.getColorForLevel(level); + return `color: #fff; background: ${bgColor}; border-radius: 4px; padding: 2px 4px;`; + } + + private getPrefixForLevel(level: LogLevel): string { + switch (level) { + case LogLevel.DEBUG: + return 'debug'; + case LogLevel.INFO: + return 'info '; + case LogLevel.WARN: + return 'warn '; + case LogLevel.ERROR: + return 'error'; + default: + return 'NOT_FOUND'; + } + } + private getColorForLevel(level: LogLevel): string { switch (level) { case LogLevel.DEBUG: return '#00a5fe'; + case LogLevel.INFO: + return '#008000'; case LogLevel.WARN: - return 'orange'; + return '#de9000'; case LogLevel.ERROR: return '#e53935'; default: From 74c6ab19a17c62e09056572a9b9259d273dd016f Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 23 Jul 2020 17:48:04 +0200 Subject: [PATCH 04/62] Split settings into StaticSettings and SettingsService. The first is the format SettingsServer the last (new one) will be used for the admin settings. --- .vscode/settings.json | 2 +- server/src/database/database.module.ts | 4 +- server/src/database/models/grading.model.ts | 4 +- server/src/database/models/models.module.ts | 2 + server/src/database/models/settings.model.ts | 5 + server/src/database/models/student.model.ts | 4 +- server/src/database/models/user.model.ts | 4 +- server/src/helpers/CollectionName.ts | 1 + server/src/module/settings/settings.module.ts | 2 +- .../module/settings/settings.service.spec.ts | 17 +- .../src/module/settings/settings.service.ts | 336 +----------------- server/src/module/settings/settings.static.ts | 315 ++++++++++++++++ .../module/template/template.service.spec.ts | 22 +- server/test/helpers/test.module.ts | 26 +- 14 files changed, 391 insertions(+), 353 deletions(-) create mode 100644 server/src/database/models/settings.model.ts create mode 100644 server/src/module/settings/settings.static.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4e17945e6..61c773adc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,6 @@ "debug.node.autoAttach": "off", "npm.packageManager": "yarn", "editor.codeActionsOnSave": { - "source.organizeImports": false + "source.organizeImports": true } } diff --git a/server/src/database/database.module.ts b/server/src/database/database.module.ts index d96d7b53c..39c5578f8 100644 --- a/server/src/database/database.module.ts +++ b/server/src/database/database.module.ts @@ -2,7 +2,7 @@ import { DynamicModule, Global, Logger, Module, Provider } from '@nestjs/common' import mongoose, { Connection } from 'mongoose'; import { getConnectionToken } from 'nestjs-typegoose'; import { TYPEGOOSE_CONNECTION_NAME } from 'nestjs-typegoose/dist/typegoose.constants'; -import { SettingsService } from '../module/settings/settings.service'; +import { StaticSettings } from '../module/settings/settings.static'; @Global() @Module({}) @@ -30,7 +30,7 @@ export class DatabaseModule { private static connectToDB(): Promise { return new Promise((resolve, reject) => { - const databaseConfig = SettingsService.getService().getDatabaseConfiguration(); + const databaseConfig = StaticSettings.getService().getDatabaseConfiguration(); const maxRetries = databaseConfig.maxRetries ?? 2; async function tryToConnect(prevTries: number) { diff --git a/server/src/database/models/grading.model.ts b/server/src/database/models/grading.model.ts index c4574fdaf..c027b12d2 100644 --- a/server/src/database/models/grading.model.ts +++ b/server/src/database/models/grading.model.ts @@ -10,7 +10,7 @@ import { } from '@typegoose/typegoose'; import { fieldEncryption } from 'mongoose-field-encryption'; import { CollectionName } from '../../helpers/CollectionName'; -import { SettingsService } from '../../module/settings/settings.service'; +import { StaticSettings } from '../../module/settings/settings.static'; import { ExerciseGradingDTO, GradingDTO } from '../../module/student/student.dto'; import { IExerciseGrading, IGrading } from '../../shared/model/Points'; import { ExerciseDocument, SubExerciseDocument } from './exercise.model'; @@ -119,7 +119,7 @@ export class ExerciseGradingModel { @modelOptions({ schemaOptions: { collection: CollectionName.GRADING } }) @plugin(fieldEncryption, { - secret: SettingsService.getSecret(), + secret: StaticSettings.getService().getDatabaseSecret(), fields: ['comment', 'additionalPoints', 'exerciseGradings'], }) export class GradingModel { diff --git a/server/src/database/models/models.module.ts b/server/src/database/models/models.module.ts index 17fd0d336..5289c4826 100644 --- a/server/src/database/models/models.module.ts +++ b/server/src/database/models/models.module.ts @@ -4,6 +4,7 @@ import { AttendanceModel } from './attendance.model'; import { GradingModel } from './grading.model'; import { ScheincriteriaModel } from './scheincriteria.model'; import { ScheinexamModel } from './scheinexam.model'; +import { SettingsModel } from './settings.model'; import { SheetModel } from './sheet.model'; import { StudentModel } from './student.model'; import { TeamModel } from './team.model'; @@ -25,6 +26,7 @@ export class ModelsModule { ScheincriteriaModel, GradingModel, SubstituteModel, + SettingsModel, ]); return { diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts new file mode 100644 index 000000000..c988c0199 --- /dev/null +++ b/server/src/database/models/settings.model.ts @@ -0,0 +1,5 @@ +import { modelOptions } from '@typegoose/typegoose'; +import { CollectionName } from '../../helpers/CollectionName'; + +@modelOptions({ schemaOptions: { collection: CollectionName.SETTINGS } }) +export class SettingsModel {} diff --git a/server/src/database/models/student.model.ts b/server/src/database/models/student.model.ts index 563d3166c..4221f6595 100644 --- a/server/src/database/models/student.model.ts +++ b/server/src/database/models/student.model.ts @@ -3,7 +3,7 @@ import { DateTime } from 'luxon'; import mongooseAutoPopulate from 'mongoose-autopopulate'; import { EncryptedDocument, fieldEncryption } from 'mongoose-field-encryption'; import { CollectionName } from '../../helpers/CollectionName'; -import { SettingsService } from '../../module/settings/settings.service'; +import { StaticSettings } from '../../module/settings/settings.static'; import { IAttendance } from '../../shared/model/Attendance'; import { IGrading } from '../../shared/model/Points'; import { IStudent, StudentStatus } from '../../shared/model/Student'; @@ -37,7 +37,7 @@ export async function populateStudentDocument(doc?: StudentDocument): Promise { type AssignableFields = Omit, 'tutorials' | 'tutorialsToCorrect'>; @plugin(fieldEncryption, { - secret: SettingsService.getSecret(), + secret: StaticSettings.getService().getDatabaseSecret(), fields: ['firstname', 'lastname', 'temporaryPassword', 'password', 'email', 'roles'], }) @plugin(mongooseAutopopulate) diff --git a/server/src/helpers/CollectionName.ts b/server/src/helpers/CollectionName.ts index 5c9ab3870..e65438e4e 100644 --- a/server/src/helpers/CollectionName.ts +++ b/server/src/helpers/CollectionName.ts @@ -7,4 +7,5 @@ export enum CollectionName { SHEET = 'sheets', SCHEINEXAM = 'scheinexams', SCHEINCRITERIA = 'scheincriterias', + SETTINGS = 'settings', } diff --git a/server/src/module/settings/settings.module.ts b/server/src/module/settings/settings.module.ts index f63ce073b..5a242616f 100644 --- a/server/src/module/settings/settings.module.ts +++ b/server/src/module/settings/settings.module.ts @@ -1,4 +1,4 @@ -import { Module, Global } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { SettingsService } from './settings.service'; @Global() diff --git a/server/src/module/settings/settings.service.spec.ts b/server/src/module/settings/settings.service.spec.ts index 9001518d8..a21559510 100644 --- a/server/src/module/settings/settings.service.spec.ts +++ b/server/src/module/settings/settings.service.spec.ts @@ -1,15 +1,26 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { TestModule } from '../../../test/helpers/test.module'; import { SettingsService } from './settings.service'; describe('SettingsService', () => { + let testModule: TestingModule; let service: SettingsService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ + beforeAll(async () => { + testModule = await Test.createTestingModule({ + imports: [TestModule.forRootAsync()], providers: [SettingsService], }).compile(); + }); + + afterAll(async () => { + await testModule.close(); + }); + + beforeEach(async () => { + await testModule.get(TestModule).reset(); - service = module.get(SettingsService); + service = testModule.get(SettingsService); }); it('should be defined', () => { diff --git a/server/src/module/settings/settings.service.ts b/server/src/module/settings/settings.service.ts index 516ef8fbf..f0f03db0c 100644 --- a/server/src/module/settings/settings.service.ts +++ b/server/src/module/settings/settings.service.ts @@ -1,326 +1,18 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { plainToClass } from 'class-transformer'; -import { validateSync, ValidationError } from 'class-validator'; -import fs from 'fs'; -import { ConnectionOptions } from 'mongoose'; -import path from 'path'; -import YAML from 'yaml'; -import { StartUpException } from '../../exceptions/StartUpException'; -import { ApplicationConfiguration } from './model/ApplicationConfiguration'; -import { DatabaseConfiguration } from './model/DatabaseConfiguration'; -import { EnvironmentConfig, ENV_VARIABLE_NAMES } from './model/EnvironmentConfig'; -import { MailingConfiguration } from './model/MailingConfiguration'; - -export interface DatabaseConfig { - databaseURL: string; - secret: string; - config: ConnectionOptions; - maxRetries?: number; -} +import { Injectable } from '@nestjs/common'; +import { ReturnModelType } from '@typegoose/typegoose'; +import { InjectModel } from 'nestjs-typegoose'; +import { SettingsModel } from '../../database/models/settings.model'; +import { StaticSettings } from './settings.static'; @Injectable() -export class SettingsService { - private static service: SettingsService | undefined; - - private readonly API_PREFIX = 'api'; - private readonly STATIC_FOLDER = 'app'; - - private readonly logger = new Logger(SettingsService.name); - - private readonly config: ApplicationConfiguration; - private readonly databaseConfig: Readonly; - private readonly envConfig: EnvironmentConfig; - - constructor() { - this.config = this.loadConfigFile(); - - this.envConfig = this.loadEnvironmentVariables(); - this.databaseConfig = this.loadDatabaseConfig(); - - SettingsService.service = this; - } - - /** - * Returns the currently active service. - * - * If no service was previously created a new one is created and returned. - * - * @returns Current SettingsService. - */ - static getService(): SettingsService { - if (!SettingsService.service) { - return new SettingsService(); - } - - return SettingsService.service; - } - - /** - * Returns the encryption secret. - * - * If no SettingsService was previously created a new one is created. - * - * @returns Encrytion secret. - */ - static getSecret(): string { - const service = this.getService(); - - return service.getDatabaseConfiguration().secret; - } - - /** - * @returns Configuration for the database. - */ - getDatabaseConfiguration(): DatabaseConfig { - return this.databaseConfig; - } - - /** - * Returns the value of the `sessionTimeout` setting. - * - * If no `sessionTimeout` configuration was provided or if the provided configuration is invalid than a default value of 120 minutes is being return. - * - * @returns The specified session timeout in _minutes_. - */ - getSessionTimeout(): number { - if (this.config.sessionTimeout !== undefined) { - return this.config.sessionTimeout; - } else { - this.logger.warn( - "No 'sessionTimeout' setting was provided. Falling back to a default value of 120 minutes." - ); - - return 120; - } - } - - /** - * Returns the prefix for the app if there is one. - * - * If there is one any trailing '/' will be removed before returning the prefix. - * - * @returns Prefix for the app or `null` if none is provided. - */ - getPathPrefix(): string | null { - const { prefix } = this.config; - if (!prefix) { - return null; - } - - return prefix.endsWith('/') ? prefix.substr(0, prefix.length - 1) : prefix; - } - - /** - * @returns Path to the configuration folder. - */ - getConfigPath(): string { - return path.join(process.cwd(), 'config'); - } - - /** - * @returns Prefix for the API path. - */ - getAPIPrefix(): string { - const pathPrefix = this.getPathPrefix(); - - if (pathPrefix) { - return `${pathPrefix}/${this.API_PREFIX}`.replace('//', '/'); - } else { - return this.API_PREFIX; - } - } - - /** - * @returns Path to the static folder. - */ - getStaticFolder(): string { - return this.STATIC_FOLDER; - } - - /** - * @returns Configuration for the mailing service. - */ - getMailingConfiguration(): MailingConfiguration { - return this.config.mailing; - } - - /** - * Loads the configuration of the database. - * - * This is achieved by combining the required options from the config-file and the required environment variables. - * - * @returns Configuration options for the database. - */ - private loadDatabaseConfig(): DatabaseConfig { - const configFromFile: DatabaseConfiguration = this.config.database; - - return { - databaseURL: configFromFile.databaseURL, - maxRetries: configFromFile.maxRetries, - secret: this.envConfig.secret, - config: { - ...configFromFile.config, - auth: { - user: this.envConfig.mongoDbUser, - password: this.envConfig.mongoDbPassword, - }, - }, - }; - } - - /** - * Loads the environment variables as `EnvironmentConfig`. If not all required variables were provided a `StartUpException` is thrown. - * - * @returns Valid configuration extracted from the environment variables. - * @throws `StartUpException` - If not all required environment variables were provided. - */ - private loadEnvironmentVariables(): EnvironmentConfig { - const envConfig = plainToClass(EnvironmentConfig, process.env, { - excludeExtraneousValues: true, - }); - - this.assertEnvNoErrors(validateSync(envConfig)); - - return envConfig; - } - - /** - * Checks if the `errors` array is empty. If it is not a StartUpExpcetion with a proper message is thrown. - * - * @param errors Array containing validation errors from class-validator (or empty). - * @throws `StartUpException` - If `errors` is not empty. - */ - private assertEnvNoErrors(errors: ValidationError[]) { - if (errors.length === 0) { - return; - } - - let message: string = 'The following environment variables are not set:'; - - for (const error of errors) { - const nameOfEnvVariable: string = (ENV_VARIABLE_NAMES as any)[error.property]; - - message += `\n- ${nameOfEnvVariable}`; - } - - throw new StartUpException(message); - } - - /** - * Checks if the `errors` array is empty. - * - * If it is not empty an exception is thrown. - * - * @param errors Array containing the validation errors. - * - * @throws `StartUpException` - If the `errors` array is not empty. - */ - private assertConfigNoErrors(errors: ValidationError[]) { - if (errors.length === 0) { - return; - } - - let message: string = 'Validation for configuration failed. For more details see below:'; - - for (const error of errors) { - message += '\n' + this.getStringForError(error); - } - - throw new StartUpException(message); - } - - /** - * Converts the given ValidationError to a formatted String. - * - * @param error Error to convert to a string. - * @param depth Current recursion depth. Used for appending an appropriate "\t" infront of the message. - * - * @returns String for the given ValidationError. - */ - private getStringForError(error: ValidationError, depth: number = 1): string { - const { property, children, constraints } = error; - let tabs: string = ''; - - for (let i = 0; i < depth; i++) { - tabs += '\t'; - } - - let message = `The following validation error(s) occured for the "${property}" property:`; - - if (children.length > 0) { - for (const childError of children) { - message += '\n' + tabs + this.getStringForError(childError, depth + 1); - } - } else { - if (!!constraints) { - for (const [constraint, msg] of Object.entries(constraints)) { - message += '\n' + tabs + `\t${constraint} - ${msg}`; - } - } else { - message += '\n' + tabs + '\tUnknown error'; - } - } - - return message; - } - - /** - * Loads the configuration file and returns the parsed configuration. - * - * The configuration file for the current environment gets loaded and parsed. - * - * @returns Parsed and validated ApplicationConfiguration. - * @throws `StartUpException` - If the configuration file does not exist or if the configuration is not valid. - */ - private loadConfigFile(): ApplicationConfiguration { - const environment = process.env.NODE_ENV || 'development'; - const filePath = path.join(this.getConfigPath(), `${environment}.yml`); - - try { - const content = fs.readFileSync(filePath, { encoding: 'utf8' }).toString(); - - return this.initConfig(content, environment); - } catch (err) { - if (err instanceof StartUpException) { - throw err; - } else { - this.logger.error(err); - - throw new StartUpException( - `Could not load the configuration for the "${environment}" environment. Make sure a corresponding configuration file exists and is readable.` - ); - } - } - } - - /** - * Loads the configuration from the given file content. - * - * If the content is not a valid configuration or valid YAML an exception is thrown. - * - * @param fileContent Content of the config file. Needs to be a valid YAML string and must contain a valid configuration. - * @param environment Value of the NodeJS environment setting. - * - * @returns Parsed and validated ApplicationConfiguration. - * @throws `StartUpException` - If either the YAML or the configuration is not valid. - */ - private initConfig(fileContent: string, environment: string): ApplicationConfiguration { - try { - const configString = YAML.parse(fileContent); - const config = plainToClass(ApplicationConfiguration, configString); - - this.assertConfigNoErrors(validateSync(config)); - - this.logger.log(`Configuration loaded for "${environment}" environment`); - return config; - } catch (err) { - if (err instanceof StartUpException) { - throw err; - } else { - throw new StartUpException( - `The loaded configuration for the ${environment} environment is not a valid YAML file.` - ); - } - } +export class SettingsService extends StaticSettings { + constructor( + @InjectModel(SettingsModel) + private readonly settingsModel: ReturnModelType + ) { + super(); + + // Overwrite the logger context from the parent class. + this.logger.setContext(SettingsService.name); } } diff --git a/server/src/module/settings/settings.static.ts b/server/src/module/settings/settings.static.ts new file mode 100644 index 000000000..86381991a --- /dev/null +++ b/server/src/module/settings/settings.static.ts @@ -0,0 +1,315 @@ +import { Logger } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; +import { validateSync, ValidationError } from 'class-validator'; +import fs from 'fs'; +import { ConnectionOptions } from 'mongoose'; +import path from 'path'; +import YAML from 'yaml'; +import { StartUpException } from '../../exceptions/StartUpException'; +import { ApplicationConfiguration } from './model/ApplicationConfiguration'; +import { DatabaseConfiguration } from './model/DatabaseConfiguration'; +import { EnvironmentConfig, ENV_VARIABLE_NAMES } from './model/EnvironmentConfig'; +import { MailingConfiguration } from './model/MailingConfiguration'; + +export interface DatabaseConfig { + databaseURL: string; + secret: string; + config: ConnectionOptions; + maxRetries?: number; +} + +export class StaticSettings { + private static service: StaticSettings = new StaticSettings(); + + private readonly API_PREFIX = 'api'; + private readonly STATIC_FOLDER = 'app'; + + protected readonly logger = new Logger(StaticSettings.name); + + private readonly config: ApplicationConfiguration; + private readonly databaseConfig: Readonly; + private readonly envConfig: EnvironmentConfig; + + constructor() { + this.config = this.loadConfigFile(); + + this.envConfig = this.loadEnvironmentVariables(); + this.databaseConfig = this.loadDatabaseConfig(); + } + + /** + * Returns the currently active service. + * + * If no service was previously created a new one is created and returned. + * + * @returns Current SettingsService. + */ + static getService(): StaticSettings { + return StaticSettings.service; + } + + /** + * Returns the encryption secret for the database. + * + * @returns Encrytion secret. + */ + getDatabaseSecret(): string { + return this.databaseConfig.secret; + } + + /** + * @returns Configuration for the database. + */ + getDatabaseConfiguration(): DatabaseConfig { + return this.databaseConfig; + } + + /** + * Returns the value of the `sessionTimeout` setting. + * + * If no `sessionTimeout` configuration was provided or if the provided configuration is invalid than a default value of 120 minutes is being return. + * + * @returns The specified session timeout in _minutes_. + */ + getSessionTimeout(): number { + if (this.config.sessionTimeout !== undefined) { + return this.config.sessionTimeout; + } else { + this.logger.warn( + "No 'sessionTimeout' setting was provided. Falling back to a default value of 120 minutes." + ); + + return 120; + } + } + + /** + * Returns the prefix for the app if there is one. + * + * If there is one any trailing '/' will be removed before returning the prefix. + * + * @returns Prefix for the app or `null` if none is provided. + */ + getPathPrefix(): string | null { + const { prefix } = this.config; + if (!prefix) { + return null; + } + + return prefix.endsWith('/') ? prefix.substr(0, prefix.length - 1) : prefix; + } + + /** + * @returns Path to the configuration folder. + */ + getConfigPath(): string { + return path.join(process.cwd(), 'config'); + } + + /** + * @returns Prefix for the API path. + */ + getAPIPrefix(): string { + const pathPrefix = this.getPathPrefix(); + + if (pathPrefix) { + return `${pathPrefix}/${this.API_PREFIX}`.replace('//', '/'); + } else { + return this.API_PREFIX; + } + } + + /** + * @returns Path to the static folder. + */ + getStaticFolder(): string { + return this.STATIC_FOLDER; + } + + /** + * @returns Configuration for the mailing service. + */ + getMailingConfiguration(): MailingConfiguration { + return this.config.mailing; + } + + /** + * Loads the configuration of the database. + * + * This is achieved by combining the required options from the config-file and the required environment variables. + * + * @returns Configuration options for the database. + */ + private loadDatabaseConfig(): DatabaseConfig { + const configFromFile: DatabaseConfiguration = this.config.database; + + return { + databaseURL: configFromFile.databaseURL, + maxRetries: configFromFile.maxRetries, + secret: this.envConfig.secret, + config: { + ...configFromFile.config, + auth: { + user: this.envConfig.mongoDbUser, + password: this.envConfig.mongoDbPassword, + }, + }, + }; + } + + /** + * Loads the environment variables as `EnvironmentConfig`. If not all required variables were provided a `StartUpException` is thrown. + * + * @returns Valid configuration extracted from the environment variables. + * @throws `StartUpException` - If not all required environment variables were provided. + */ + private loadEnvironmentVariables(): EnvironmentConfig { + const envConfig = plainToClass(EnvironmentConfig, process.env, { + excludeExtraneousValues: true, + }); + + this.assertEnvNoErrors(validateSync(envConfig)); + + return envConfig; + } + + /** + * Checks if the `errors` array is empty. If it is not a StartUpExpcetion with a proper message is thrown. + * + * @param errors Array containing validation errors from class-validator (or empty). + * @throws `StartUpException` - If `errors` is not empty. + */ + private assertEnvNoErrors(errors: ValidationError[]) { + if (errors.length === 0) { + return; + } + + let message: string = 'The following environment variables are not set:'; + + for (const error of errors) { + const nameOfEnvVariable: string = (ENV_VARIABLE_NAMES as any)[error.property]; + + message += `\n- ${nameOfEnvVariable}`; + } + + throw new StartUpException(message); + } + + /** + * Checks if the `errors` array is empty. + * + * If it is not empty an exception is thrown. + * + * @param errors Array containing the validation errors. + * + * @throws `StartUpException` - If the `errors` array is not empty. + */ + private assertConfigNoErrors(errors: ValidationError[]) { + if (errors.length === 0) { + return; + } + + let message: string = 'Validation for configuration failed. For more details see below:'; + + for (const error of errors) { + message += '\n' + this.getStringForError(error); + } + + throw new StartUpException(message); + } + + /** + * Converts the given ValidationError to a formatted String. + * + * @param error Error to convert to a string. + * @param depth Current recursion depth. Used for appending an appropriate "\t" infront of the message. + * + * @returns String for the given ValidationError. + */ + private getStringForError(error: ValidationError, depth: number = 1): string { + const { property, children, constraints } = error; + let tabs: string = ''; + + for (let i = 0; i < depth; i++) { + tabs += '\t'; + } + + let message = `The following validation error(s) occured for the "${property}" property:`; + + if (children.length > 0) { + for (const childError of children) { + message += '\n' + tabs + this.getStringForError(childError, depth + 1); + } + } else { + if (!!constraints) { + for (const [constraint, msg] of Object.entries(constraints)) { + message += '\n' + tabs + `\t${constraint} - ${msg}`; + } + } else { + message += '\n' + tabs + '\tUnknown error'; + } + } + + return message; + } + + /** + * Loads the configuration file and returns the parsed configuration. + * + * The configuration file for the current environment gets loaded and parsed. + * + * @returns Parsed and validated ApplicationConfiguration. + * @throws `StartUpException` - If the configuration file does not exist or if the configuration is not valid. + */ + private loadConfigFile(): ApplicationConfiguration { + const environment = process.env.NODE_ENV || 'development'; + const filePath = path.join(this.getConfigPath(), `${environment}.yml`); + + try { + const content = fs.readFileSync(filePath, { encoding: 'utf8' }).toString(); + + return this.initConfig(content, environment); + } catch (err) { + if (err instanceof StartUpException) { + throw err; + } else { + this.logger.error(err); + + throw new StartUpException( + `Could not load the configuration for the "${environment}" environment. Make sure a corresponding configuration file exists and is readable.` + ); + } + } + } + + /** + * Loads the configuration from the given file content. + * + * If the content is not a valid configuration or valid YAML an exception is thrown. + * + * @param fileContent Content of the config file. Needs to be a valid YAML string and must contain a valid configuration. + * @param environment Value of the NodeJS environment setting. + * + * @returns Parsed and validated ApplicationConfiguration. + * @throws `StartUpException` - If either the YAML or the configuration is not valid. + */ + private initConfig(fileContent: string, environment: string): ApplicationConfiguration { + try { + const configString = YAML.parse(fileContent); + const config = plainToClass(ApplicationConfiguration, configString); + + this.assertConfigNoErrors(validateSync(config)); + + this.logger.log(`Configuration loaded for "${environment}" environment`); + return config; + } catch (err) { + if (err instanceof StartUpException) { + throw err; + } else { + throw new StartUpException( + `The loaded configuration for the ${environment} environment is not a valid YAML file.` + ); + } + } + } +} diff --git a/server/src/module/template/template.service.spec.ts b/server/src/module/template/template.service.spec.ts index fc8a1afad..bcdb56c72 100644 --- a/server/src/module/template/template.service.spec.ts +++ b/server/src/module/template/template.service.spec.ts @@ -1,17 +1,27 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { SettingsModule } from '../settings/settings.module'; +import { TestModule } from '../../../test/helpers/test.module'; +import { SettingsService } from '../settings/settings.service'; import { TemplateService } from './template.service'; describe('TemplateService', () => { + let testModule: TestingModule; let service: TemplateService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [SettingsModule], - providers: [TemplateService], + beforeAll(async () => { + testModule = await Test.createTestingModule({ + imports: [TestModule.forRootAsync()], + providers: [TemplateService, SettingsService], }).compile(); + }); + + afterAll(async () => { + await testModule.close(); + }); + + beforeEach(async () => { + await testModule.get(TestModule).reset(); - service = module.get(TemplateService); + service = testModule.get(TemplateService); }); it('should be defined', () => { diff --git a/server/test/helpers/test.module.ts b/server/test/helpers/test.module.ts index 7c1c3c430..81d94ec52 100644 --- a/server/test/helpers/test.module.ts +++ b/server/test/helpers/test.module.ts @@ -4,23 +4,24 @@ import { MongoMemoryServer } from 'mongodb-memory-server'; import { Connection, Model } from 'mongoose'; import { getConnectionToken, getModelToken, TypegooseModule } from 'nestjs-typegoose'; import { TypegooseClass } from 'nestjs-typegoose/dist/typegoose-class.interface'; +import { GradingModel } from '../../src/database/models/grading.model'; +import { ScheincriteriaModel } from '../../src/database/models/scheincriteria.model'; +import { ScheinexamModel } from '../../src/database/models/scheinexam.model'; +import { SettingsModel } from '../../src/database/models/settings.model'; +import { SheetModel } from '../../src/database/models/sheet.model'; +import { StudentModel } from '../../src/database/models/student.model'; +import { TeamModel } from '../../src/database/models/team.model'; +import { TutorialModel } from '../../src/database/models/tutorial.model'; import { UserModel } from '../../src/database/models/user.model'; import { - USER_DOCUMENTS, - TUTORIAL_DOCUMENTS, - STUDENT_DOCUMENTS, - SHEET_DOCUMENTS, - SCHEINEXAM_DOCUMENTS, SCHEINCRITERIA_DOCUMENTS, + SCHEINEXAM_DOCUMENTS, + SHEET_DOCUMENTS, + STUDENT_DOCUMENTS, TEAM_DOCUMENTS, + TUTORIAL_DOCUMENTS, + USER_DOCUMENTS, } from '../mocks/documents.mock'; -import { TutorialModel } from '../../src/database/models/tutorial.model'; -import { StudentModel } from '../../src/database/models/student.model'; -import { TeamModel } from '../../src/database/models/team.model'; -import { SheetModel } from '../../src/database/models/sheet.model'; -import { ScheinexamModel } from '../../src/database/models/scheinexam.model'; -import { ScheincriteriaModel } from '../../src/database/models/scheincriteria.model'; -import { GradingModel } from '../../src/database/models/grading.model'; interface ModelMockOptions { model: TypegooseClass; @@ -36,6 +37,7 @@ const MODEL_OPTIONS: ModelMockOptions[] = [ { model: ScheinexamModel, initialDocuments: [...SCHEINEXAM_DOCUMENTS] }, { model: ScheincriteriaModel, initialDocuments: [...SCHEINCRITERIA_DOCUMENTS] }, { model: GradingModel, initialDocuments: [] }, + { model: SettingsModel, initialDocuments: [] }, ]; @Module({}) From c9aac00408847af708e6c916d49e150913f57e44 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 23 Jul 2020 18:04:44 +0200 Subject: [PATCH 05/62] Fix deprecated typegoose arrayProp and mapProp decorators. --- server/src/database/models/exercise.model.ts | 4 ++-- server/src/database/models/grading.model.ts | 16 ++++------------ server/src/database/models/scheinexam.model.ts | 4 ++-- server/src/database/models/sheet.model.ts | 4 ++-- server/src/database/models/student.model.ts | 8 ++++---- server/src/database/models/team.model.ts | 4 ++-- server/src/database/models/tutorial.model.ts | 12 ++++++------ server/src/database/models/user.model.ts | 10 +++++----- 8 files changed, 27 insertions(+), 35 deletions(-) diff --git a/server/src/database/models/exercise.model.ts b/server/src/database/models/exercise.model.ts index 67a27a8ea..edec4f922 100644 --- a/server/src/database/models/exercise.model.ts +++ b/server/src/database/models/exercise.model.ts @@ -1,4 +1,4 @@ -import { arrayProp, DocumentType, mongoose, prop } from '@typegoose/typegoose'; +import { DocumentType, mongoose, prop } from '@typegoose/typegoose'; import { generateObjectId } from '../../helpers/generateObjectId'; import { ExerciseDTO, SubExerciseDTO } from '../../module/sheet/sheet.dto'; import { ExercisePointInfo } from '../../shared/model/Points'; @@ -114,7 +114,7 @@ export class ExerciseModel { ); } - @arrayProp({ default: [], items: SubExerciseModel }) + @prop({ default: [], type: SubExerciseModel }) subexercises!: SubExerciseDocument[]; static fromDTO(dto: ExerciseDTO): ExerciseModel { diff --git a/server/src/database/models/grading.model.ts b/server/src/database/models/grading.model.ts index c027b12d2..f852e763b 100644 --- a/server/src/database/models/grading.model.ts +++ b/server/src/database/models/grading.model.ts @@ -1,13 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { - arrayProp, - DocumentType, - getModelForClass, - mapProp, - modelOptions, - plugin, - prop, -} from '@typegoose/typegoose'; +import { DocumentType, getModelForClass, modelOptions, plugin, prop } from '@typegoose/typegoose'; import { fieldEncryption } from 'mongoose-field-encryption'; import { CollectionName } from '../../helpers/CollectionName'; import { StaticSettings } from '../../module/settings/settings.static'; @@ -50,7 +42,7 @@ export class ExerciseGradingModel { this._points = newPoints; } - @mapProp({ of: Number }) + @prop({ type: Number }) subExercisePoints?: Map; getGradingForSubexercise(subExercise: SubExerciseDocument): number | undefined { @@ -157,10 +149,10 @@ export class GradingModel { @prop() additionalPoints?: number; - @mapProp({ of: ExerciseGradingModel, autopopulate: true, default: new Map() }) + @prop({ type: ExerciseGradingModel, autopopulate: true, default: new Map() }) exerciseGradings!: Map; - @arrayProp({ ref: StudentModel, autopopulate: true, default: [] }) + @prop({ ref: StudentModel, autopopulate: true, default: [] }) students!: StudentDocument[]; /** diff --git a/server/src/database/models/scheinexam.model.ts b/server/src/database/models/scheinexam.model.ts index 5b6841ac1..a3ca43ffa 100644 --- a/server/src/database/models/scheinexam.model.ts +++ b/server/src/database/models/scheinexam.model.ts @@ -1,4 +1,4 @@ -import { arrayProp, DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; import { DateTime } from 'luxon'; import mongooseAutoPopulate from 'mongoose-autopopulate'; import { CollectionName } from '../../helpers/CollectionName'; @@ -31,7 +31,7 @@ export class ScheinexamModel { this._date = date.toISODate(); } - @arrayProp({ required: true, items: ExerciseModel }) + @prop({ required: true, type: ExerciseModel }) exercises!: ExerciseDocument[]; @prop({ required: true }) diff --git a/server/src/database/models/sheet.model.ts b/server/src/database/models/sheet.model.ts index 5924aedd9..d6a5f20af 100644 --- a/server/src/database/models/sheet.model.ts +++ b/server/src/database/models/sheet.model.ts @@ -1,4 +1,4 @@ -import { arrayProp, DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; import mongooseAutoPopulate from 'mongoose-autopopulate'; import { CollectionName } from '../../helpers/CollectionName'; import { ISheet } from '../../shared/model/Sheet'; @@ -26,7 +26,7 @@ export class SheetModel { @prop({ required: true }) bonusSheet!: boolean; - @arrayProp({ required: true, items: ExerciseModel }) + @prop({ required: true, type: ExerciseModel }) exercises!: ExerciseDocument[]; get totalPoints(): number { diff --git a/server/src/database/models/student.model.ts b/server/src/database/models/student.model.ts index 4221f6595..3a9b18f47 100644 --- a/server/src/database/models/student.model.ts +++ b/server/src/database/models/student.model.ts @@ -1,4 +1,4 @@ -import { arrayProp, DocumentType, mapProp, modelOptions, plugin, prop } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; import { DateTime } from 'luxon'; import mongooseAutoPopulate from 'mongoose-autopopulate'; import { EncryptedDocument, fieldEncryption } from 'mongoose-field-encryption'; @@ -81,15 +81,15 @@ export class StudentModel { @prop({ default: 0 }) cakeCount!: number; - @mapProp({ of: AttendanceModel, autopopulate: true, default: new Map() }) + @prop({ type: AttendanceModel, autopopulate: true, default: new Map() }) attendances!: Map; - @arrayProp({ ref: 'GradingModel', foreignField: 'students', localField: '_id' }) + @prop({ ref: 'GradingModel', foreignField: 'students', localField: '_id' }) private _gradings!: GradingDocument[]; private gradings?: Map; - @mapProp({ of: Number, default: new Map() }) + @prop({ type: Number, default: new Map() }) presentationPoints!: Map; /** diff --git a/server/src/database/models/team.model.ts b/server/src/database/models/team.model.ts index 29ed24887..2159a6064 100644 --- a/server/src/database/models/team.model.ts +++ b/server/src/database/models/team.model.ts @@ -1,4 +1,4 @@ -import { arrayProp, DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; import mongooseAutoPopulate from 'mongoose-autopopulate'; import { CollectionName } from '../../helpers/CollectionName'; import { NoFunctions } from '../../helpers/NoFunctions'; @@ -41,7 +41,7 @@ export class TeamModel { @prop({ required: true, autopopulate: true, ref: TutorialModel }) tutorial!: TutorialDocument; - @arrayProp({ + @prop({ ref: 'StudentModel', foreignField: 'team', localField: '_id', diff --git a/server/src/database/models/tutorial.model.ts b/server/src/database/models/tutorial.model.ts index 0214d88bd..cd0c1e199 100644 --- a/server/src/database/models/tutorial.model.ts +++ b/server/src/database/models/tutorial.model.ts @@ -1,4 +1,4 @@ -import { arrayProp, DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; import { DateTime, ToISOTimeOptions } from 'luxon'; import { Schema } from 'mongoose'; import mongooseAutoPopulate from 'mongoose-autopopulate'; @@ -58,7 +58,7 @@ export class TutorialModel { @prop({ ref: 'UserModel', autopopulate: true }) tutor?: UserDocument; - @arrayProp({ required: true, items: Schema.Types.String }) + @prop({ required: true, type: Schema.Types.String }) private _dates!: string[]; get dates(): DateTime[] { @@ -91,24 +91,24 @@ export class TutorialModel { this._endTime = endTime.startOf('minute').toISOTime({ suppressMilliseconds: true }); } - @arrayProp({ + @prop({ ref: 'StudentModel', foreignField: 'tutorial', localField: '_id', }) students!: StudentDocument[]; - @arrayProp({ + @prop({ ref: 'TeamModel', foreignField: 'tutorial', localField: '_id', }) teams!: TeamDocument[]; - @arrayProp({ ref: 'UserModel', autopopulate: true, default: [] }) + @prop({ ref: 'UserModel', autopopulate: true, default: [] }) correctors!: UserDocument[]; - @arrayProp({ type: SubstituteModel, autopopulate: true, default: [] }) + @prop({ type: SubstituteModel, autopopulate: true, default: [] }) private _substitutes!: SubstituteDocument[]; private substitutes?: Map; diff --git a/server/src/database/models/user.model.ts b/server/src/database/models/user.model.ts index 84c6a2dfc..3eada8067 100644 --- a/server/src/database/models/user.model.ts +++ b/server/src/database/models/user.model.ts @@ -1,4 +1,4 @@ -import { arrayProp, DocumentType, modelOptions, plugin, pre, prop } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, plugin, pre, prop } from '@typegoose/typegoose'; import bcrypt from 'bcryptjs'; import mongooseAutopopulate from 'mongoose-autopopulate'; import { EncryptedDocument, fieldEncryption } from 'mongoose-field-encryption'; @@ -61,10 +61,10 @@ export class UserModel { @prop({ required: true }) lastname!: string; - @arrayProp({ required: true, items: String }) + @prop({ required: true, type: String }) roles!: Role[]; - @prop({ required: true, unique: true }) + @prop({ required: true }) username!: string; @prop({ required: true }) @@ -76,14 +76,14 @@ export class UserModel { @prop() temporaryPassword?: string; - @arrayProp({ + @prop({ ref: 'TutorialModel', foreignField: 'tutor', localField: '_id', }) tutorials!: TutorialDocument[]; - @arrayProp({ + @prop({ ref: 'TutorialModel', foreignField: 'correctors', localField: '_id', From 405114a0ea459b455c970b9b5f2b636e8b96158d Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 23 Jul 2020 18:19:20 +0200 Subject: [PATCH 06/62] Add some basic logic to the SettingsService. --- server/src/database/models/settings.model.ts | 9 +++- .../src/module/settings/settings.service.ts | 50 +++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index c988c0199..2b5ccc7cb 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -1,5 +1,10 @@ -import { modelOptions } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, prop } from '@typegoose/typegoose'; import { CollectionName } from '../../helpers/CollectionName'; @modelOptions({ schemaOptions: { collection: CollectionName.SETTINGS } }) -export class SettingsModel {} +export class SettingsModel { + @prop() + defaultTeamSize?: number; +} + +export type SettingsDocument = DocumentType; diff --git a/server/src/module/settings/settings.service.ts b/server/src/module/settings/settings.service.ts index f0f03db0c..bb2bab6e6 100644 --- a/server/src/module/settings/settings.service.ts +++ b/server/src/module/settings/settings.service.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { ReturnModelType } from '@typegoose/typegoose'; import { InjectModel } from 'nestjs-typegoose'; -import { SettingsModel } from '../../database/models/settings.model'; +import { SettingsDocument, SettingsModel } from '../../database/models/settings.model'; import { StaticSettings } from './settings.static'; @Injectable() -export class SettingsService extends StaticSettings { +export class SettingsService extends StaticSettings implements OnModuleInit { constructor( @InjectModel(SettingsModel) private readonly settingsModel: ReturnModelType @@ -15,4 +15,48 @@ export class SettingsService extends StaticSettings { // Overwrite the logger context from the parent class. this.logger.setContext(SettingsService.name); } + + /** + * Checks if there is a `SettingsDocument` in the database. + * + * If there is NO settings document after module initialization a new default `SettingsDocument` is created. + * + * If there is ONE nothing is done. + */ + async onModuleInit(): Promise { + const document = await this.settingsModel.find(); + + if (document.length === 0) { + this.logger.log('No settings document provided. Creating new default settings document...'); + + // TODO: Implement default document creation! + throw new Error('Not implemented'); + } + } + + /** + * Find the settings document in the database. + * + * Finds and returns the `SettingsDocument` saved in the database. If there is more than one the first document is returned. + * + * @returns `SettingsDocument` if there is one, `undefined` else. + * @throws `Error` - If there is not `SettingsDocument` saved in the database. + */ + async getSettingsDocument(): Promise { + const documents = await this.settingsModel.find(); + + if (documents.length > 1) { + this.logger.warn( + 'More than one settings document was found. Using the first entry in the database.' + ); + } + + if (!documents[0]) { + throw new Error( + 'No settings document saved in the database. This might be due to a failed initialization.' + ); + } + + return documents[0]; + } } From 7f6366859095e2097df167b4c0658af4c370fff1 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jul 2020 01:00:06 +0200 Subject: [PATCH 07/62] Add ability to provide default config through config file. This allows the admin to define the default settings before the first start of the server. --- server/config/development.yml | 4 ++ server/src/database/models/settings.model.ts | 9 ++++ .../model/ApplicationConfiguration.ts | 18 ++++++- .../settings/model/MailingConfiguration.ts | 2 +- .../src/module/settings/settings.service.ts | 47 ++++++++++++------- server/src/module/settings/settings.static.ts | 33 +++++-------- 6 files changed, 73 insertions(+), 40 deletions(-) diff --git a/server/config/development.yml b/server/config/development.yml index f33a13a97..8319696be 100644 --- a/server/config/development.yml +++ b/server/config/development.yml @@ -12,6 +12,10 @@ database: authSource: 'admin' authMechanism: 'SCRAM-SHA-1' +defaultSettings: + canTutorExcuseStudents: true + defaultTeamSize: 3 + mailing: testingMode: true host: HOST diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index 2b5ccc7cb..bb088493c 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -1,10 +1,19 @@ import { DocumentType, modelOptions, prop } from '@typegoose/typegoose'; import { CollectionName } from '../../helpers/CollectionName'; +import { NoFunctions } from '../../helpers/NoFunctions'; @modelOptions({ schemaOptions: { collection: CollectionName.SETTINGS } }) export class SettingsModel { @prop() defaultTeamSize?: number; + + @prop() + canTutorExcuseStudents?: boolean; + + constructor(fields?: NoFunctions) { + this.defaultTeamSize = fields?.defaultTeamSize; + this.canTutorExcuseStudents = fields?.canTutorExcuseStudents; + } } export type SettingsDocument = DocumentType; diff --git a/server/src/module/settings/model/ApplicationConfiguration.ts b/server/src/module/settings/model/ApplicationConfiguration.ts index 623c589f4..f638c251d 100644 --- a/server/src/module/settings/model/ApplicationConfiguration.ts +++ b/server/src/module/settings/model/ApplicationConfiguration.ts @@ -1,8 +1,19 @@ import { Type } from 'class-transformer'; -import { IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator'; +import { IsBoolean, IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator'; import { DatabaseConfiguration } from './DatabaseConfiguration'; import { MailingConfiguration } from './MailingConfiguration'; +class DefaultSettingsConfiguration { + @IsNumber() + @Min(1) + @IsOptional() + defaultTeamSize?: number; + + @IsBoolean() + @IsOptional() + canTutorExcuseStudents?: boolean; +} + export class ApplicationConfiguration { @IsOptional() @IsNumber() @@ -20,4 +31,9 @@ export class ApplicationConfiguration { @Type(() => MailingConfiguration) @ValidateNested() readonly mailing!: MailingConfiguration; + + @IsOptional() + @Type(() => DefaultSettingsConfiguration) + @ValidateNested() + readonly defaultSettings: DefaultSettingsConfiguration | undefined; } diff --git a/server/src/module/settings/model/MailingConfiguration.ts b/server/src/module/settings/model/MailingConfiguration.ts index a5f191c87..4d1aff6de 100644 --- a/server/src/module/settings/model/MailingConfiguration.ts +++ b/server/src/module/settings/model/MailingConfiguration.ts @@ -1,5 +1,5 @@ -import { ValidateNested, IsString, IsBoolean, IsNumber } from 'class-validator'; import { Type } from 'class-transformer'; +import { IsBoolean, IsNumber, IsString, ValidateNested } from 'class-validator'; export class MailingAuthConfiguration { @IsString() diff --git a/server/src/module/settings/settings.service.ts b/server/src/module/settings/settings.service.ts index bb2bab6e6..58f6c2103 100644 --- a/server/src/module/settings/settings.service.ts +++ b/server/src/module/settings/settings.service.ts @@ -2,6 +2,7 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { ReturnModelType } from '@typegoose/typegoose'; import { InjectModel } from 'nestjs-typegoose'; import { SettingsDocument, SettingsModel } from '../../database/models/settings.model'; +import { StartUpException } from '../../exceptions/StartUpException'; import { StaticSettings } from './settings.static'; @Injectable() @@ -16,24 +17,6 @@ export class SettingsService extends StaticSettings implements OnModuleInit { this.logger.setContext(SettingsService.name); } - /** - * Checks if there is a `SettingsDocument` in the database. - * - * If there is NO settings document after module initialization a new default `SettingsDocument` is created. - * - * If there is ONE nothing is done. - */ - async onModuleInit(): Promise { - const document = await this.settingsModel.find(); - - if (document.length === 0) { - this.logger.log('No settings document provided. Creating new default settings document...'); - - // TODO: Implement default document creation! - throw new Error('Not implemented'); - } - } - /** * Find the settings document in the database. * @@ -59,4 +42,32 @@ export class SettingsService extends StaticSettings implements OnModuleInit { return documents[0]; } + + /** + * Checks if there is a `SettingsDocument` in the database. + * + * If there is NO settings document after module initialization a new default `SettingsDocument` is created. + * + * If there is ONE nothing is done. + */ + async onModuleInit(): Promise { + const document = await this.settingsModel.find(); + + if (document.length === 0) { + this.logger.log('No settings document provided. Creating new default settings document...'); + + try { + const defaultsFromConfig = this.config.defaultSettings; + const defaultSettings = new SettingsModel({ + defaultTeamSize: defaultsFromConfig?.defaultTeamSize ?? 2, + canTutorExcuseStudents: defaultsFromConfig?.canTutorExcuseStudents ?? false, + }); + await this.settingsModel.create(defaultSettings); + + this.logger.log('Default settings document successfully created.'); + } catch (err) { + throw new StartUpException('Could not create the default settings document.'); + } + } + } } diff --git a/server/src/module/settings/settings.static.ts b/server/src/module/settings/settings.static.ts index 86381991a..0ef43f799 100644 --- a/server/src/module/settings/settings.static.ts +++ b/server/src/module/settings/settings.static.ts @@ -2,7 +2,6 @@ import { Logger } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { validateSync, ValidationError } from 'class-validator'; import fs from 'fs'; -import { ConnectionOptions } from 'mongoose'; import path from 'path'; import YAML from 'yaml'; import { StartUpException } from '../../exceptions/StartUpException'; @@ -11,25 +10,21 @@ import { DatabaseConfiguration } from './model/DatabaseConfiguration'; import { EnvironmentConfig, ENV_VARIABLE_NAMES } from './model/EnvironmentConfig'; import { MailingConfiguration } from './model/MailingConfiguration'; -export interface DatabaseConfig { - databaseURL: string; - secret: string; - config: ConnectionOptions; - maxRetries?: number; -} +type DatabaseConfig = DatabaseConfiguration & { secret: string }; export class StaticSettings { private static service: StaticSettings = new StaticSettings(); - private readonly API_PREFIX = 'api'; - private readonly STATIC_FOLDER = 'app'; - - protected readonly logger = new Logger(StaticSettings.name); + private static readonly API_PREFIX = 'api'; + private static readonly STATIC_FOLDER = 'app'; - private readonly config: ApplicationConfiguration; + // TODO: Redo these properties to have a more readable 'loading flow'. + protected readonly config: ApplicationConfiguration; private readonly databaseConfig: Readonly; private readonly envConfig: EnvironmentConfig; + protected readonly logger = new Logger(StaticSettings.name); + constructor() { this.config = this.loadConfigFile(); @@ -113,9 +108,9 @@ export class StaticSettings { const pathPrefix = this.getPathPrefix(); if (pathPrefix) { - return `${pathPrefix}/${this.API_PREFIX}`.replace('//', '/'); + return `${pathPrefix}/${StaticSettings.API_PREFIX}`.replace('//', '/'); } else { - return this.API_PREFIX; + return StaticSettings.API_PREFIX; } } @@ -123,7 +118,7 @@ export class StaticSettings { * @returns Path to the static folder. */ getStaticFolder(): string { - return this.STATIC_FOLDER; + return StaticSettings.STATIC_FOLDER; } /** @@ -149,10 +144,8 @@ export class StaticSettings { secret: this.envConfig.secret, config: { ...configFromFile.config, - auth: { - user: this.envConfig.mongoDbUser, - password: this.envConfig.mongoDbPassword, - }, + user: this.envConfig.mongoDbUser, + pass: this.envConfig.mongoDbPassword, }, }; } @@ -307,7 +300,7 @@ export class StaticSettings { throw err; } else { throw new StartUpException( - `The loaded configuration for the ${environment} environment is not a valid YAML file.` + `The loaded configuration for the ${environment} environment is not a valid YAML file: ${err}` ); } } From 81a82173e37f6a9c3d12bc88f8f6015f0a02bf52 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jul 2020 12:35:32 +0200 Subject: [PATCH 08/62] Add proper handling of default settings. This combines the ones from the config file with the internal ones used as a fallback. --- server/src/database/models/settings.model.ts | 30 ++++++--- .../model/ApplicationConfiguration.ts | 6 +- .../module/settings/settings.controller.ts | 14 ++++ server/src/module/settings/settings.module.ts | 7 +- .../src/module/settings/settings.service.ts | 65 ++++++++++++------- server/src/shared/model/Settings.ts | 5 ++ 6 files changed, 91 insertions(+), 36 deletions(-) create mode 100644 server/src/module/settings/settings.controller.ts create mode 100644 server/src/shared/model/Settings.ts diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index bb088493c..afd0fc9a0 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -1,18 +1,32 @@ import { DocumentType, modelOptions, prop } from '@typegoose/typegoose'; import { CollectionName } from '../../helpers/CollectionName'; -import { NoFunctions } from '../../helpers/NoFunctions'; @modelOptions({ schemaOptions: { collection: CollectionName.SETTINGS } }) export class SettingsModel { - @prop() - defaultTeamSize?: number; + private static get internalDefaults(): ISettings { + return { defaultTeamSize: 2, canTutorExcuseStudents: false }; + } + + @prop({ required: true }) + defaultTeamSize: number; + + @prop({ required: true }) + canTutorExcuseStudents: boolean; + + constructor(fields?: Partial) { + this.defaultTeamSize = + fields?.defaultTeamSize ?? SettingsModel.internalDefaults.defaultTeamSize; + this.canTutorExcuseStudents = + fields?.canTutorExcuseStudents ?? SettingsModel.internalDefaults.canTutorExcuseStudents; + } - @prop() - canTutorExcuseStudents?: boolean; + toDTO(): ISettings { + const defaultSettings = SettingsModel.internalDefaults; - constructor(fields?: NoFunctions) { - this.defaultTeamSize = fields?.defaultTeamSize; - this.canTutorExcuseStudents = fields?.canTutorExcuseStudents; + return { + defaultTeamSize: this.defaultTeamSize ?? defaultSettings.defaultTeamSize, + canTutorExcuseStudents: this.canTutorExcuseStudents ?? defaultSettings.canTutorExcuseStudents, + }; } } diff --git a/server/src/module/settings/model/ApplicationConfiguration.ts b/server/src/module/settings/model/ApplicationConfiguration.ts index f638c251d..bb73d1419 100644 --- a/server/src/module/settings/model/ApplicationConfiguration.ts +++ b/server/src/module/settings/model/ApplicationConfiguration.ts @@ -3,7 +3,7 @@ import { IsBoolean, IsNumber, IsOptional, IsString, Min, ValidateNested } from ' import { DatabaseConfiguration } from './DatabaseConfiguration'; import { MailingConfiguration } from './MailingConfiguration'; -class DefaultSettingsConfiguration { +class DefaultSettings implements Partial { @IsNumber() @Min(1) @IsOptional() @@ -33,7 +33,7 @@ export class ApplicationConfiguration { readonly mailing!: MailingConfiguration; @IsOptional() - @Type(() => DefaultSettingsConfiguration) + @Type(() => DefaultSettings) @ValidateNested() - readonly defaultSettings: DefaultSettingsConfiguration | undefined; + readonly defaultSettings: DefaultSettings | undefined; } diff --git a/server/src/module/settings/settings.controller.ts b/server/src/module/settings/settings.controller.ts new file mode 100644 index 000000000..3477eaa77 --- /dev/null +++ b/server/src/module/settings/settings.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { AuthenticatedGuard } from '../../guards/authenticated.guard'; +import { SettingsService } from './settings.service'; + +@Controller('settings') +export class SettingsController { + constructor(private readonly settingsService: SettingsService) {} + + @Get('/') + @UseGuards(AuthenticatedGuard) + async getAllSettings(): Promise { + return this.settingsService.getAllSettings(); + } +} diff --git a/server/src/module/settings/settings.module.ts b/server/src/module/settings/settings.module.ts index 5a242616f..67dc4df29 100644 --- a/server/src/module/settings/settings.module.ts +++ b/server/src/module/settings/settings.module.ts @@ -1,6 +1,11 @@ import { Global, Module } from '@nestjs/common'; +import { SettingsController } from './settings.controller'; import { SettingsService } from './settings.service'; @Global() -@Module({ providers: [SettingsService], exports: [SettingsService] }) +@Module({ + providers: [SettingsService], + exports: [SettingsService], + controllers: [SettingsController], +}) export class SettingsModule {} diff --git a/server/src/module/settings/settings.service.ts b/server/src/module/settings/settings.service.ts index 58f6c2103..c6bcc4627 100644 --- a/server/src/module/settings/settings.service.ts +++ b/server/src/module/settings/settings.service.ts @@ -17,6 +17,41 @@ export class SettingsService extends StaticSettings implements OnModuleInit { this.logger.setContext(SettingsService.name); } + /** + * @returns The current settings. + * + * @see getSettingsDocument + */ + async getAllSettings(): Promise { + const document = await this.getSettingsDocument(); + return document.toDTO(); + } + + /** + * Checks if there is a `SettingsDocument` in the database. + * + * If there is NO settings document after module initialization a new default `SettingsDocument` is created. + * + * If there is ONE nothing is done. + */ + async onModuleInit(): Promise { + const document = await this.settingsModel.find(); + + if (document.length === 0) { + this.logger.log('No settings document provided. Creating new default settings document...'); + + try { + const defaults = this.generateDefaultSettings(); + this.logger.log(`Default settings used: ${JSON.stringify(defaults)}`); + await this.settingsModel.create(defaults); + + this.logger.log('Default settings document successfully created.'); + } catch (err) { + throw new StartUpException('Could not create the default settings document.'); + } + } + } + /** * Find the settings document in the database. * @@ -25,7 +60,7 @@ export class SettingsService extends StaticSettings implements OnModuleInit { * @returns `SettingsDocument` if there is one, `undefined` else. * @throws `Error` - If there is not `SettingsDocument` saved in the database. */ - async getSettingsDocument(): Promise { + private async getSettingsDocument(): Promise { const documents = await this.settingsModel.find(); if (documents.length > 1) { @@ -44,30 +79,12 @@ export class SettingsService extends StaticSettings implements OnModuleInit { } /** - * Checks if there is a `SettingsDocument` in the database. + * Generates a `SettingsModel` with some default values. * - * If there is NO settings document after module initialization a new default `SettingsDocument` is created. - * - * If there is ONE nothing is done. + * The defaults can be overwritten by the config file. If they are present in said file those values will be used. */ - async onModuleInit(): Promise { - const document = await this.settingsModel.find(); - - if (document.length === 0) { - this.logger.log('No settings document provided. Creating new default settings document...'); - - try { - const defaultsFromConfig = this.config.defaultSettings; - const defaultSettings = new SettingsModel({ - defaultTeamSize: defaultsFromConfig?.defaultTeamSize ?? 2, - canTutorExcuseStudents: defaultsFromConfig?.canTutorExcuseStudents ?? false, - }); - await this.settingsModel.create(defaultSettings); - - this.logger.log('Default settings document successfully created.'); - } catch (err) { - throw new StartUpException('Could not create the default settings document.'); - } - } + private generateDefaultSettings(): SettingsModel { + const defaultsFromConfig = this.config.defaultSettings; + return new SettingsModel(defaultsFromConfig); } } diff --git a/server/src/shared/model/Settings.ts b/server/src/shared/model/Settings.ts new file mode 100644 index 000000000..ef8abbad3 --- /dev/null +++ b/server/src/shared/model/Settings.ts @@ -0,0 +1,5 @@ +interface ISettings { + defaultTeamSize: number; + + canTutorExcuseStudents: boolean; +} From 35db1bb0e2e454371738157a4d8db02999034f9c Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jul 2020 12:49:14 +0200 Subject: [PATCH 09/62] Fix overflow issue with the internals management. --- .../TutorialInternalsManagement.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/view/tutorial-internals-management/TutorialInternalsManagement.tsx b/client/src/view/tutorial-internals-management/TutorialInternalsManagement.tsx index 03279c118..8a7d27e33 100644 --- a/client/src/view/tutorial-internals-management/TutorialInternalsManagement.tsx +++ b/client/src/view/tutorial-internals-management/TutorialInternalsManagement.tsx @@ -129,7 +129,13 @@ function Content({ routes, tutorial, isLoading, error, onBackClick }: Props): JS - + Date: Fri, 24 Jul 2020 17:15:47 +0200 Subject: [PATCH 10/62] Fix issues with new version of @types/luxon. --- .../src/database/models/attendance.model.ts | 17 ++++++++--- .../src/database/models/scheinexam.model.ts | 8 +++-- server/src/database/models/student.model.ts | 8 ++++- server/src/database/models/tutorial.model.ts | 30 ++++++++++++++----- server/src/module/excel/excel.service.ts | 6 +++- server/src/module/user/user.service.ts | 9 ++++-- 6 files changed, 60 insertions(+), 18 deletions(-) diff --git a/server/src/database/models/attendance.model.ts b/server/src/database/models/attendance.model.ts index c7221f9df..fbdc7e2b2 100644 --- a/server/src/database/models/attendance.model.ts +++ b/server/src/database/models/attendance.model.ts @@ -1,7 +1,7 @@ -import { DocumentType, modelOptions, prop, getModelForClass } from '@typegoose/typegoose'; +import { DocumentType, getModelForClass, modelOptions, prop } from '@typegoose/typegoose'; import { DateTime } from 'luxon'; -import { AttendanceState, IAttendance } from '../../shared/model/Attendance'; import { AttendanceDTO } from '../../module/student/student.dto'; +import { AttendanceState, IAttendance } from '../../shared/model/Attendance'; interface ConstructorFields { date: Date | string; @@ -25,7 +25,11 @@ export class AttendanceModel { } set date(date: DateTime) { - this._date = date.toISODate(); + const parsed = date.toISODate(); + + if (!!parsed) { + this._date = parsed; + } } @prop() @@ -43,9 +47,14 @@ export class AttendanceModel { toDTO(this: AttendanceDocument): IAttendance { const { date, note, state } = this; + const parsedDate = date.toISODate(); + + if (!parsedDate) { + throw new Error(`Inner date object could not be parsed to ISODate.`); + } return { - date: date.toISODate(), + date: parsedDate, note, state, }; diff --git a/server/src/database/models/scheinexam.model.ts b/server/src/database/models/scheinexam.model.ts index a3ca43ffa..e09f2c559 100644 --- a/server/src/database/models/scheinexam.model.ts +++ b/server/src/database/models/scheinexam.model.ts @@ -28,7 +28,11 @@ export class ScheinexamModel { } set date(date: DateTime) { - this._date = date.toISODate(); + const parsed = date.toISODate(); + + if (!!parsed) { + this._date = parsed; + } } @prop({ required: true, type: ExerciseModel }) @@ -99,7 +103,7 @@ export class ScheinexamModel { id: this.id, scheinExamNo: this.scheinExamNo, percentageNeeded: this.percentageNeeded, - date: this.date.toISODate(), + date: this.date.toISODate() ?? 'DATE_NOT_PARSEABLE', exercises: this.exercises.map((ex) => ex.toDTO()), }; } diff --git a/server/src/database/models/student.model.ts b/server/src/database/models/student.model.ts index 3a9b18f47..07a92621d 100644 --- a/server/src/database/models/student.model.ts +++ b/server/src/database/models/student.model.ts @@ -243,7 +243,13 @@ export class StudentModel { } private getDateKey(date: DateTime): string { - return date.toISODate(); + const dateKey = date.toISODate(); + + if (!dateKey) { + throw new Error(`Date '${date}' is not parseable to an ISODate.`); + } + + return dateKey; } } diff --git a/server/src/database/models/tutorial.model.ts b/server/src/database/models/tutorial.model.ts index cd0c1e199..7af4d11ad 100644 --- a/server/src/database/models/tutorial.model.ts +++ b/server/src/database/models/tutorial.model.ts @@ -66,7 +66,9 @@ export class TutorialModel { } set dates(dates: DateTime[]) { - this._dates = dates.map((date) => date.toISODate()); + this._dates = dates + .map((date) => date.toISODate()) + .filter((val): val is string => val !== null); } @prop({ required: true }) @@ -77,7 +79,11 @@ export class TutorialModel { } set startTime(startTime: DateTime) { - this._startTime = startTime.startOf('minute').toISOTime({ suppressMilliseconds: true }); + const time = startTime.startOf('minute').toISOTime({ suppressMilliseconds: true }); + + if (time) { + this._startTime = time; + } } @prop({ required: true }) @@ -88,7 +94,11 @@ export class TutorialModel { } set endTime(endTime: DateTime) { - this._endTime = endTime.startOf('minute').toISOTime({ suppressMilliseconds: true }); + const time = endTime.startOf('minute').toISOTime({ suppressMilliseconds: true }); + + if (time) { + this._endTime = time; + } } @prop({ @@ -211,9 +221,9 @@ export class TutorialModel { tutor: tutor ? { id: tutor.id, firstname: tutor.firstname, lastname: tutor.lastname } : undefined, - dates: dates.map((date) => date.toISODate()), - startTime: startTime.toISOTime(dateOptions), - endTime: endTime.toISOTime(dateOptions), + dates: dates.map((date) => date.toISODate() ?? 'DATE_NOT_PARSEABLE'), + startTime: startTime.toISOTime(dateOptions) ?? 'DATE_NOT_PARSABLE', + endTime: endTime.toISOTime(dateOptions) ?? 'DATE_NOT_PARSEBALE', students: students.map((student) => student.id), correctors: correctors.map((corrector) => ({ id: corrector.id, @@ -226,7 +236,13 @@ export class TutorialModel { } private getDateKey(date: DateTime): string { - return date.toISODate(); + const dateKey = date.toISODate(); + + if (!dateKey) { + throw new Error(`Date '${date}' is not parseable to ISODate.`); + } + + return dateKey; } } diff --git a/server/src/module/excel/excel.service.ts b/server/src/module/excel/excel.service.ts index c8845a7fc..08c660ac4 100644 --- a/server/src/module/excel/excel.service.ts +++ b/server/src/module/excel/excel.service.ts @@ -245,8 +245,12 @@ export class ExcelService { for (const date of tutorial.dates) { const dateKey = date.toISO(); + if (!dateKey) { + throw new Error(`Date '${date}' could not be parsed to an ISODate`); + } + headers[dateKey] = { - name: date.toISODate(), + name: date.toISODate() ?? 'DATE_NOT_PARSEABLE', column, }; data[dateKey] = []; diff --git a/server/src/module/user/user.service.ts b/server/src/module/user/user.service.ts index c06708b79..7377c86b3 100644 --- a/server/src/module/user/user.service.ts +++ b/server/src/module/user/user.service.ts @@ -15,10 +15,10 @@ import { UserCredentialsWithPassword } from '../../auth/auth.model'; import { TutorialDocument } from '../../database/models/tutorial.model'; import { populateUserDocument, UserDocument, UserModel } from '../../database/models/user.model'; import { CRUDService } from '../../helpers/CRUDService'; +import { NamedElement } from '../../shared/model/Common'; import { Role } from '../../shared/model/Role'; import { TutorialService } from '../tutorial/tutorial.service'; import { CreateUserDTO, UserDTO } from './user.dto'; -import { NamedElement } from '../../shared/model/Common'; @Injectable() export class UserService implements OnModuleInit, CRUDService { @@ -326,15 +326,18 @@ export class UserService implements OnModuleInit, CRUDService Date: Fri, 24 Jul 2020 17:16:15 +0200 Subject: [PATCH 11/62] Change sessions to be saved in the MongoDB. This replaces the (default) usage of the MemoryStore of express-session. --- server/package.json | 1 + server/src/setupApp.ts | 20 +- yarn.lock | 651 +++++++++++++++++++++-------------------- 3 files changed, 347 insertions(+), 325 deletions(-) diff --git a/server/package.json b/server/package.json index 82f995d00..b7d382daa 100644 --- a/server/package.json +++ b/server/package.json @@ -60,6 +60,7 @@ "@nestjs/schematics": "^7.0.1", "@nestjs/testing": "^7.3.2", "@types/bcryptjs": "^2.4.2", + "@types/connect-mongo": "^3.1.3", "@types/express": "^4.17.7", "@types/express-session": "^1.17.0", "@types/jest": "^26.0.4", diff --git a/server/src/setupApp.ts b/server/src/setupApp.ts index 9fa88b807..7c3e378ac 100644 --- a/server/src/setupApp.ts +++ b/server/src/setupApp.ts @@ -1,12 +1,14 @@ import { INestApplication, Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import ConnectMongo from 'connect-mongo'; +import session from 'express-session'; +import { getConnectionToken } from 'nestjs-typegoose'; +import passport from 'passport'; import { AppModule } from './app.module'; import { NotFoundExceptionFilter } from './filter/not-found-exception.filter'; import { isDevelopment } from './helpers/isDevelopment'; import { SettingsService } from './module/settings/settings.service'; -import session = require('express-session'); -import passport = require('passport'); /** * Sets up the security middleware. @@ -16,17 +18,23 @@ import passport = require('passport'); * @param app The application itself */ function initSecurityMiddleware(app: INestApplication) { - const loggerContext = 'Init session'; + const loggerContext = 'Init security'; + Logger.log('Setting up security middleware...', loggerContext); + const settings = app.get(SettingsService); - Logger.log('Setting up passport...', loggerContext); + const connection = app.get(getConnectionToken()); + const secret = settings.getDatabaseConfiguration().secret; const timeoutSetting = settings.getSessionTimeout(); + const MongoStore = ConnectMongo(session); + Logger.log(`Setting timeout to: ${timeoutSetting} minutes`, loggerContext); app.use( session({ - secret: settings.getDatabaseConfiguration().secret, + secret, + store: new MongoStore({ secret, mongooseConnection: connection, ttl: timeoutSetting * 60 }), resave: false, // Is used to extend the expries date on every request. This means, maxAge is relative to the time of the last request of a user. rolling: true, @@ -41,7 +49,7 @@ function initSecurityMiddleware(app: INestApplication) { app.use(passport.initialize()); app.use(passport.session()); - Logger.log('Passport setup complete.', loggerContext); + Logger.log('Security middleware setup complete.', loggerContext); } /** diff --git a/yarn.lock b/yarn.lock index dc54a727c..32daebc11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,9 +70,9 @@ "@babel/highlight" "^7.10.4" "@babel/compat-data@^7.10.4", "@babel/compat-data@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.4.tgz#706a6484ee6f910b719b696a9194f8da7d7ac241" - integrity sha512-t+rjExOrSVvjQQXNp5zAIYDp00KjdvGl/TpDX5REPr0S9IAIPQMTilcfG6q8c0QFmj9lSTVySV2VTsyggvtNIw== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.5.tgz#d38425e67ea96b1480a3f50404d1bf85676301a6" + integrity sha512-mPVoWNzIpYJHbWje0if7Ck36bpbtTvIxOi9+6WSK9wjGEXearAqlwBoTQvVjsAY2VIwgcs8V940geY3okzRCEw== dependencies: browserslist "^4.12.0" invariant "^2.2.4" @@ -101,35 +101,34 @@ source-map "^0.5.0" "@babel/core@^7.1.0", "@babel/core@^7.4.5", "@babel/core@^7.7.5": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.4.tgz#780e8b83e496152f8dd7df63892b2e052bf1d51d" - integrity sha512-3A0tS0HWpy4XujGc7QtOIHTeNwUgWaZc/WuS5YQrfhU67jnVmsD6OGPc1AKHH0LJHQICGncy3+YUjIhVlfDdcA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330" + integrity sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.4" + "@babel/generator" "^7.10.5" + "@babel/helper-module-transforms" "^7.10.5" "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.10.4" + "@babel/parser" "^7.10.5" "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/traverse" "^7.10.5" + "@babel/types" "^7.10.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" json5 "^2.1.2" - lodash "^4.17.13" + lodash "^4.17.19" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.10.4", "@babel/generator@^7.4.0", "@babel/generator@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.4.tgz#e49eeed9fe114b62fa5b181856a43a5e32f5f243" - integrity sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng== +"@babel/generator@^7.10.5", "@babel/generator@^7.4.0", "@babel/generator@^7.9.0": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" + integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.10.5" jsesc "^2.5.1" - lodash "^4.17.13" source-map "^0.5.0" "@babel/helper-annotate-as-pure@^7.10.4": @@ -148,13 +147,13 @@ "@babel/types" "^7.10.4" "@babel/helper-builder-react-jsx-experimental@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.4.tgz#d0ffb875184d749c63ffe1f4f65be15143ec322d" - integrity sha512-LyacH/kgQPgLAuaWrvvq1+E7f5bLyT8jXCh7nM67sRsy2cpIGfgWJ+FCnAKQXfY+F0tXUaN6FqLkp4JiCzdK8Q== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" + integrity sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-module-imports" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/types" "^7.10.5" "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" @@ -175,13 +174,13 @@ levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.4.tgz#2d4015d0136bd314103a70d84a7183e4b344a355" - integrity sha512-9raUiOsXPxzzLjCXeosApJItoMnX3uyT4QdM2UldffuGApNrF8e938MwNpDCK9CPoyxrEoCgT+hObJc3mZa6lQ== +"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5", "@babel/helper-create-class-features-plugin@^7.8.3": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" "@babel/helper-optimise-call-expression" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" @@ -197,13 +196,13 @@ regexpu-core "^4.7.0" "@babel/helper-define-map@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.4.tgz#f037ad794264f729eda1889f4ee210b870999092" - integrity sha512-nIij0oKErfCnLUCWaCaHW0Bmtl2RO9cN7+u2QT8yqTywgALKlyUVOvHDElh+b5DwVC6YB1FOYFOTWcN/+41EDA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" + integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/types" "^7.10.4" - lodash "^4.17.13" + "@babel/types" "^7.10.5" + lodash "^4.17.19" "@babel/helper-explode-assignable-expression@^7.10.4": version "7.10.4" @@ -236,12 +235,12 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.4.tgz#7cd04b57dfcf82fce9aeae7d4e4452fa31b8c7c4" - integrity sha512-m5j85pK/KZhuSdM/8cHUABQTAslV47OjfIB9Cc7P+PvlAoBzdb79BGNfw8RhT5Mq3p+xGd0ZfAKixbrUZx0C7A== +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee" + integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.10.5" "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3": version "7.10.4" @@ -250,18 +249,18 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d" - integrity sha512-Er2FQX0oa3nV7eM1o0tNCTx7izmQtwAQsIiaLRWtavAAEcskb0XJ5OjJbVrYXWOTr8om921Scabn4/tzlx7j1Q== +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.9.0": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6" + integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA== dependencies: "@babel/helper-module-imports" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-simple-access" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - lodash "^4.17.13" + "@babel/types" "^7.10.5" + lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" @@ -276,11 +275,11 @@ integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== "@babel/helper-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.4.tgz#59b373daaf3458e5747dece71bbaf45f9676af6d" - integrity sha512-inWpnHGgtg5NOF0eyHlC0/74/VkdRITY9dtTpB2PrxKKn+AkVMRiZz/Adrx+Ssg+MLDesi2zohBW6MVq6b4pOQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" + integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== dependencies: - lodash "^4.17.13" + lodash "^4.17.19" "@babel/helper-remap-async-to-generator@^7.10.4": version "7.10.4" @@ -351,15 +350,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.7.0", "@babel/parser@^7.9.0", "@babel/parser@^7.9.6": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.4.tgz#9eedf27e1998d87739fb5028a5120557c06a1a64" - integrity sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA== +"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.7.0", "@babel/parser@^7.9.0", "@babel/parser@^7.9.6": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" + integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== "@babel/plugin-proposal-async-generator-functions@^7.10.4", "@babel/plugin-proposal-async-generator-functions@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.4.tgz#4b65abb3d9bacc6c657aaa413e56696f9f170fc6" - integrity sha512-MJbxGSmejEFVOANAezdO39SObkURO5o/8b6fSH6D1pi9RZQt+ldppKPXfqgUWpSQ9asM6xaSaSJIaeWMDRP0Zg== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" + integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-remap-async-to-generator" "^7.10.4" @@ -630,12 +629,11 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-block-scoping@^7.10.4", "@babel/plugin-transform-block-scoping@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.4.tgz#a670d1364bb5019a621b9ea2001482876d734787" - integrity sha512-J3b5CluMg3hPUii2onJDRiaVbPtKFPLEaV5dOPY5OeAbDi1iU/UbbFFTgwb7WnanaDy7bjU35kc26W3eM5Qa0A== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.5.tgz#b81b8aafefbfe68f0f65f7ef397b9ece68a6037d" + integrity sha512-6Ycw3hjpQti0qssQcA6AMSFDHeNJ++R6dIMnpRqUjFeBBTmTDPa8zgF90OVfTvAo11mXZTlVUViY1g8ffrURLg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - lodash "^4.17.13" "@babel/plugin-transform-classes@^7.10.4", "@babel/plugin-transform-classes@^7.9.0": version "7.10.4" @@ -726,11 +724,11 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-modules-amd@^7.10.4", "@babel/plugin-transform-modules-amd@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.4.tgz#cb407c68b862e4c1d13a2fc738c7ec5ed75fc520" - integrity sha512-3Fw+H3WLUrTlzi3zMiZWp3AR4xadAEMv6XRCYnd5jAlLM61Rn+CRJaZMaNvIpcJpQ3vs1kyifYvEVPFfoSkKOA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" + integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== dependencies: - "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" @@ -745,12 +743,12 @@ babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-systemjs@^7.10.4", "@babel/plugin-transform-modules-systemjs@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.4.tgz#8f576afd943ac2f789b35ded0a6312f929c633f9" - integrity sha512-Tb28LlfxrTiOTGtZFsvkjpyjCl9IoaRI52AEU/VIwOwvDQWtbNJsAqTXzh+5R7i74e/OZHH2c2w2fsOqAfnQYQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" + integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== dependencies: "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" @@ -785,9 +783,9 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.8.7": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.4.tgz#7b4d137c87ea7adc2a0f3ebf53266871daa6fced" - integrity sha512-RurVtZ/D5nYfEg0iVERXYKEgDFeesHrHfx8RT05Sq57ucj2eOYAP6eu5fynL4Adju4I/mP/I6SO0DqNWAXjfLQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" + integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== dependencies: "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" @@ -838,9 +836,9 @@ "@babel/plugin-syntax-jsx" "^7.10.4" "@babel/plugin-transform-react-jsx-source@^7.10.4", "@babel/plugin-transform-react-jsx-source@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.4.tgz#86baf0fcccfe58084e06446a80858e1deae8f291" - integrity sha512-FTK3eQFrPv2aveerUSazFmGygqIdTtvskG50SnGnbEUnRPcGx2ylBhdFIzoVS1ty44hEgcPoCAyw5r3VDEq+Ug== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4" + integrity sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" @@ -910,9 +908,9 @@ "@babel/helper-regex" "^7.10.4" "@babel/plugin-transform-template-literals@^7.10.4", "@babel/plugin-transform-template-literals@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.4.tgz#e6375407b30fcb7fcfdbba3bb98ef3e9d36df7bc" - integrity sha512-4NErciJkAYe+xI5cqfS8pV/0ntlY5N5Ske/4ImxAVX7mk9Rxt2bwDTGv1Msc2BRJvWQcmYEC+yoMLdX22aE4VQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" + integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" @@ -925,11 +923,11 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-typescript@^7.10.4", "@babel/plugin-transform-typescript@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.4.tgz#8b01cb8d77f795422277cc3fcf45af72bc68ba78" - integrity sha512-3WpXIKDJl/MHoAN0fNkSr7iHdUMHZoppXjf2HJ9/ed5Xht5wNIsXllJXdityKOxeA3Z8heYRb1D3p2H5rfCdPw== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.5.tgz#edf353944e979f40d8ff9fe4e9975d0a465037c5" + integrity sha512-YCyYsFrrRMZ3qR7wRwtSSJovPG5vGyG4ZdcSAivGwTfoasMp3VOB/AKhohu3dFtmB4cCDcsndCSxGtrdliCsZQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-create-class-features-plugin" "^7.10.5" "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-typescript" "^7.10.4" @@ -1137,9 +1135,9 @@ "@babel/plugin-transform-typescript" "^7.10.4" "@babel/runtime-corejs3@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz#f29fc1990307c4c57b10dbd6ce667b27159d9e0d" - integrity sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz#a57fe6c13045ca33768a2aa527ead795146febe1" + integrity sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A== dependencies: core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" @@ -1152,9 +1150,9 @@ regenerator-runtime "^0.13.4" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99" - integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" + integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== dependencies: regenerator-runtime "^0.13.4" @@ -1167,28 +1165,28 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.9.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.4.tgz#e642e5395a3b09cc95c8e74a27432b484b697818" - integrity sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.9.0": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" + integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.4" + "@babel/generator" "^7.10.5" "@babel/helper-function-name" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/parser" "^7.10.5" + "@babel/types" "^7.10.5" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.13" + lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.1", "@babel/types@^7.7.0", "@babel/types@^7.9.0", "@babel/types@^7.9.6": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.4.tgz#369517188352e18219981efd156bfdb199fff1ee" - integrity sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg== +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.1", "@babel/types@^7.7.0", "@babel/types@^7.9.0", "@babel/types@^7.9.6": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" + integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== dependencies: "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.13" + lodash "^4.17.19" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -1815,9 +1813,9 @@ node-fetch "^2.3.0" "@prettier/plugin-pug@^1.4.4": - version "1.4.4" - resolved "https://registry.yarnpkg.com/@prettier/plugin-pug/-/plugin-pug-1.4.4.tgz#252c8dfac574c82d1e95492b9bd4cb59c44d3ee6" - integrity sha512-Lb3cc0a4IwUy9oNeYuN1YHg/+J5JDsJNSKFMGpcp6cq6vnMcNyWn+KNBkTo/ZYEIT2JAsl6X+KKAvjH1p+XGeQ== + version "1.5.0" + resolved "https://registry.yarnpkg.com/@prettier/plugin-pug/-/plugin-pug-1.5.0.tgz#3d7208eb95781dab38d0daf7a25d550ec678e9f1" + integrity sha512-VdXe7R5hIKkW7KOmyCocJHXwNY0WwBGzM4QfdGEWLa0uxN3g/wmSmLqtCBLTcLtLCH4qULWdWwiQW2rl6UFE1Q== dependencies: pug-lexer "~5.0.0" @@ -1830,9 +1828,9 @@ "@angular-devkit/schematics" "9.1.9" "@sinonjs/commons@^1.7.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" - integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q== + version "1.8.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" + integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== dependencies: type-detect "4.0.8" @@ -1989,9 +1987,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.0.12" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.12.tgz#22f49a028e69465390f87bb103ebd61bd086b8f5" - integrity sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA== + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.13.tgz#1874914be974a492e1b4cb00585cabb274e8ba18" + integrity sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ== dependencies: "@babel/types" "^7.3.0" @@ -2020,6 +2018,13 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/connect-mongo@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/connect-mongo/-/connect-mongo-3.1.3.tgz#4ff124a30e530dd2dae4725cfd28ca0b9badbfd1" + integrity sha512-h162kWzfphobvJOttkYKLXEQQmZuOlOSA1IszOusQhguCGf+/B8k4H373SJ0BtVv+qkXP/lziEuUfZDNfzZ1tw== + dependencies: + connect-mongo "*" + "@types/connect@*": version "3.4.33" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" @@ -2067,9 +2072,9 @@ integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== "@types/express-serve-static-core@*": - version "4.17.8" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.8.tgz#b8f7b714138536742da222839892e203df569d1c" - integrity sha512-1SJZ+R3Q/7mLkOD9ewCBDYD2k0WyZQtWYqF/2VvoNN2/uhI49J9CDN4OAm+wGMA0DbArA4ef27xl4+JwMtGggw== + version "4.17.9" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz#2d7b34dcfd25ec663c25c85d76608f8b249667f1" + integrity sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA== dependencies: "@types/node" "*" "@types/qs" "*" @@ -2118,9 +2123,9 @@ get-port "*" "@types/glob@^7.1.1": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" - integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== dependencies: "@types/minimatch" "*" "@types/node" "*" @@ -2167,9 +2172,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^26.0.4": - version "26.0.4" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.4.tgz#d2e513e85aca16992816f192582b5e67b0b15efb" - integrity sha512-4fQNItvelbNA9+sFgU+fhJo8ZFF+AS4Egk3GWwCW2jFtViukXbnztccafAdLhzE/0EiCogljtQQXP8aQ9J7sFg== + version "26.0.7" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.7.tgz#495cb1d1818c1699dbc3b8b046baf1c86ef5e324" + integrity sha512-+x0077/LoN6MjqBcVOe1y9dpryWnfDZ+Xfo3EqGeBcfPRJlQp3Lw62RvNlWxuGv7kOEwlHriAa54updi3Jvvwg== dependencies: jest-diff "^25.2.1" pretty-format "^25.2.1" @@ -2195,14 +2200,14 @@ integrity sha512-65WZedEm4AnOsBDdsapJJG42MhROu3n4aSSiu87JXF/pSdlubxZxp3S1yz3kTfkJ2KBPud4CpjoHVAptOm9Zmw== "@types/lodash@^4.14.157": - version "4.14.157" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" - integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== + version "4.14.158" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.158.tgz#b38ea8b6fe799acd076d7a8d7ab71c26ef77f785" + integrity sha512-InCEXJNTv/59yO4VSfuvNrZHt7eeNtWQEgnieIA+mIC+MOWM9arOWG2eQ8Vhk6NbOre6/BidiXhkZYeDY9U35w== "@types/luxon@^1.24.1": - version "1.24.1" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.24.1.tgz#60f57209b9afd8e046161ecb8322e2875fc182ee" - integrity sha512-t93qL4l3PRxy4qQkXwiPS6qjIt7S6o90XMuCfvYDgIAQJvgSBni5qVPxkhGYPnZZPS9ASHOTeCZXRNmdiHHHcg== + version "1.24.3" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.24.3.tgz#2e360e02b081898f993c52901192224e1f3d6daa" + integrity sha512-8lkeHb0Hkpyqmj0lYrZItakKM73jaKTUe4/PMl2e8o96oxTUzagbbz2DUOMn0NpjSovmZQTJvoCheFeI2bwS4g== "@types/markdown-it@^10.0.1": version "10.0.1" @@ -2223,9 +2228,9 @@ integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== "@types/mime@*": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5" - integrity sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== "@types/minimatch@*": version "3.0.3" @@ -2256,9 +2261,9 @@ "@types/node" "*" "@types/node@*", "@types/node@^14.0.23": - version "14.0.23" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" - integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== + version "14.0.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.25.tgz#7ad8b00a1206d6c9e94810e49f3115f0bcc30456" + integrity sha512-okMqUHqrMlGOxfDZliX1yFX5MV6qcd5PpRz96XYtjkM0Ws/hwg23FMUqt6pETrVRZS+EKUB5HY19mmo54EuQbA== "@types/nodemailer@^6.4.0": version "6.4.0" @@ -2273,9 +2278,9 @@ integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== "@types/papaparse@^5.0.4": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.0.4.tgz#70792c74d9932bcc0bfa945ae7dacfef67f4ee57" - integrity sha512-jFv9NcRddMiW4+thmntwZ1AhvMDAX4+tAUDkWWbNcIzgqyjjkuSHOEUPoVh1/gqJTWfDOD1tvl+hSp88W3UtqA== + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.0.5.tgz#d74223fd280fa7934cd4baf63e0038cbe4ee81da" + integrity sha512-e3SavLfCswEZt7MAq/bYmmDfzRlecHqd37mAHTsQqlsBKfhPCHx3pK5H+gbN/ew/792yMJvfHDjfFIZhKAtPzg== dependencies: "@types/node" "*" @@ -2302,16 +2307,16 @@ "@types/passport" "*" "@types/passport@*": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.3.tgz#e459ed6c262bf0686684d1b05901be0d0b192a9c" - integrity sha512-nyztuxtDPQv9utCzU0qW7Gl8BY2Dn8BKlYAFFyxKipFxjaVd96celbkLCV/tRqqBUZ+JB8If3UfgV8347DTo3Q== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.4.tgz#1b35c4e197560d3974fa5f71711b6e9cce0711f0" + integrity sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA== dependencies: "@types/express" "*" "@types/prettier@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.1.tgz#b6e98083f13faa1e5231bfa3bdb1b0feff536b6d" - integrity sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ== + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.2.tgz#5bb52ee68d0f8efa9cc0099920e56be6cc4e37f3" + integrity sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA== "@types/prop-types@*": version "15.7.3" @@ -2435,9 +2440,9 @@ integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA== "@types/uglify-js@*": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.2.tgz#01992579debba674e1e359cd6bcb1a1d0ab2e02b" - integrity sha512-d6dIfpPbF+8B7WiCi2ELY7m0w1joD8cRW4ms88Emdb2w062NeEpbNCeWwVCgzLRpVG+5e74VFSg4rgJ2xXjEiQ== + version "3.9.3" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b" + integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w== dependencies: source-map "^0.6.1" @@ -2452,9 +2457,9 @@ integrity sha512-WAy5txG7aFX8Vw3sloEKp5p/t/Xt8jD3GRD9DacnFv6Vo8ubudAsRTXgxpQwU0mpzY/H8U4db3roDuCMjShBmw== "@types/webpack-sources@*": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.0.tgz#e58f1f05f87d39a5c64cf85705bdbdbb94d4d57e" - integrity sha512-c88dKrpSle9BtTqR6ifdaxu1Lvjsl3C5OsfvuUbUwdXymshv1TkufUAXBajCCUM/f/TmnkZC/Esb03MinzSiXQ== + version "1.4.1" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.1.tgz#3bed49013ec7935680d781e83cf4b5ce13ddf917" + integrity sha512-B/RJcbpMp1/od7KADJlW/jeXTEau6NYmhWo+hB29cEfRriYK9SRlH8sY4hI9Au7nrP95Z5MumGvIEiEBHMxoWA== dependencies: "@types/node" "*" "@types/source-list-map" "*" @@ -2514,11 +2519,11 @@ tsutils "^3.17.1" "@typescript-eslint/eslint-plugin@^3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.6.1.tgz#5ced8fd2087fbb83a76973dea4a0d39d9cb4a642" - integrity sha512-06lfjo76naNeOMDl+mWG9Fh/a0UHKLGhin+mGaIw72FUMbMGBkdi/FEJmgEDzh4eE73KIYzHWvOCYJ0ak7nrJQ== + version "3.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.7.0.tgz#0f91aa3c83d019591719e597fbdb73a59595a263" + integrity sha512-4OEcPON3QIx0ntsuiuFP/TkldmBGXf0uKxPQlGtS/W2F3ndYm8Vgdpj/woPJkzUc65gd3iR+qi3K8SDQP/obFg== dependencies: - "@typescript-eslint/experimental-utils" "3.6.1" + "@typescript-eslint/experimental-utils" "3.7.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" @@ -2535,14 +2540,14 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.6.1.tgz#b5a2738ebbceb3fa90c5b07d50bb1225403c4a54" - integrity sha512-oS+hihzQE5M84ewXrTlVx7eTgc52eu+sVmG7ayLfOhyZmJ8Unvf3osyFQNADHP26yoThFfbxcibbO0d2FjnYhg== +"@typescript-eslint/experimental-utils@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.0.tgz#0ee21f6c48b2b30c63211da23827725078d5169a" + integrity sha512-xpfXXAfZqhhqs5RPQBfAFrWDHoNxD5+sVB5A46TF58Bq1hRfVROrWHcQHHUM9aCBdy9+cwATcvCbRg8aIRbaHQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.6.1" - "@typescript-eslint/typescript-estree" "3.6.1" + "@typescript-eslint/types" "3.7.0" + "@typescript-eslint/typescript-estree" "3.7.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -2557,20 +2562,20 @@ eslint-visitor-keys "^1.1.0" "@typescript-eslint/parser@^3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.6.1.tgz#216e8adf4ee9c629f77c985476a2ea07fb80e1dc" - integrity sha512-SLihQU8RMe77YJ/jGTqOt0lMq7k3hlPVfp7v/cxMnXA9T0bQYoMDfTsNgHXpwSJM1Iq2aAJ8WqekxUwGv5F67Q== + version "3.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.7.0.tgz#3e9cd9df9ea644536feb6e5acdb8279ecff96ce9" + integrity sha512-2LZauVUt7jAWkcIW7djUc3kyW+fSarNEuM3RF2JdLHR9BfX/nDEnyA4/uWz0wseoWVZbDXDF7iF9Jc342flNqQ== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.6.1" - "@typescript-eslint/types" "3.6.1" - "@typescript-eslint/typescript-estree" "3.6.1" + "@typescript-eslint/experimental-utils" "3.7.0" + "@typescript-eslint/types" "3.7.0" + "@typescript-eslint/typescript-estree" "3.7.0" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/types@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.6.1.tgz#87600fe79a1874235d3cc1cf5c7e1a12eea69eee" - integrity sha512-NPxd5yXG63gx57WDTW1rp0cF3XlNuuFFB5G+Kc48zZ+51ZnQn9yjDEsjTPQ+aWM+V+Z0I4kuTFKjKvgcT1F7xQ== +"@typescript-eslint/types@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.7.0.tgz#09897fab0cb95479c01166b10b2c03c224821077" + integrity sha512-reCaK+hyKkKF+itoylAnLzFeNYAEktB0XVfSQvf0gcVgpz1l49Lt6Vo9x4MVCCxiDydA0iLAjTF/ODH0pbfnpg== "@typescript-eslint/typescript-estree@2.34.0": version "2.34.0" @@ -2585,13 +2590,13 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.6.1.tgz#a5c91fcc5497cce7922ff86bc37d5e5891dcdefa" - integrity sha512-G4XRe/ZbCZkL1fy09DPN3U0mR6SayIv1zSeBNquRFRk7CnVLgkC2ZPj8llEMJg5Y8dJ3T76SvTGtceytniaztQ== +"@typescript-eslint/typescript-estree@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.0.tgz#66872e6da120caa4b64e6b4ca5c8702afc74738d" + integrity sha512-xr5oobkYRebejlACGr1TJ0Z/r0a2/HUf0SXqPvlgUMwiMqOCu/J+/Dr9U3T0IxpE5oLFSkqMx1FE/dKaZ8KsOQ== dependencies: - "@typescript-eslint/types" "3.6.1" - "@typescript-eslint/visitor-keys" "3.6.1" + "@typescript-eslint/types" "3.7.0" + "@typescript-eslint/visitor-keys" "3.7.0" debug "^4.1.1" glob "^7.1.6" is-glob "^4.0.1" @@ -2599,10 +2604,10 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.6.1.tgz#5c57a7772f4dd623cfeacc219303e7d46f963b37" - integrity sha512-qC8Olwz5ZyMTZrh4Wl3K4U6tfms0R/mzU4/5W3XeUZptVraGVmbptJbn6h2Ey6Rb3hOs3zWoAUebZk8t47KGiQ== +"@typescript-eslint/visitor-keys@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.0.tgz#ac0417d382a136e4571a0b0dcfe52088cb628177" + integrity sha512-k5PiZdB4vklUpUX4NBncn5RBKty8G3ihTY+hqJsCdMuD0v4jofI5xuqwnVcWxfv6iTm2P/dfEa2wMUnsUY8ODw== dependencies: eslint-visitor-keys "^1.1.0" @@ -2961,7 +2966,7 @@ acorn@^6.0.1, acorn@^6.0.4, acorn@^6.2.1, acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.1, acorn@^7.2.0: +acorn@^7.1.1, acorn@^7.3.1: version "7.3.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== @@ -3008,9 +3013,9 @@ ajv-errors@^1.0.0: integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.0.tgz#5c894537098785926d71e696114a53ce768ed773" - integrity sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw== + version "3.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.1.tgz#b83ca89c5d42d69031f424cad49aada0236c6957" + integrity sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA== ajv@6.12.0: version "6.12.0" @@ -3023,9 +3028,9 @@ ajv@6.12.0: uri-js "^4.2.2" ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5: - version "6.12.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" - integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -3327,12 +3332,12 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^9.6.1: - version "9.8.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.4.tgz#736f1012673a70fa3464671d78d41abd54512863" - integrity sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A== + version "9.8.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa" + integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg== dependencies: browserslist "^4.12.0" - caniuse-lite "^1.0.30001087" + caniuse-lite "^1.0.30001097" colorette "^1.2.0" normalize-range "^0.1.2" num2fraction "^1.2.2" @@ -3830,12 +3835,12 @@ browserslist@4.10.0: pkg-up "^3.1.0" browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.2, browserslist@^4.6.4, browserslist@^4.8.5, browserslist@^4.9.1: - version "4.12.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.2.tgz#76653d7e4c57caa8a1a28513e2f4e197dc11a711" - integrity sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw== + version "4.13.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" + integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== dependencies: - caniuse-lite "^1.0.30001088" - electron-to-chromium "^1.3.483" + caniuse-lite "^1.0.30001093" + electron-to-chromium "^1.3.488" escalade "^3.0.1" node-releases "^1.1.58" @@ -4040,10 +4045,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001087, caniuse-lite@^1.0.30001088: - version "1.0.30001093" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001093.tgz#833e80f64b1a0455cbceed2a4a3baf19e4abd312" - integrity sha512-0+ODNoOjtWD5eS9aaIpf4K0gQqZfILNY4WSNuYzeT1sXni+lMrrVjc0odEobJt6wrODofDZUX8XYi/5y7+xl8g== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097: + version "1.0.30001105" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001105.tgz#d2cb0b31e5cf2f3ce845033b61c5c01566549abf" + integrity sha512-JupOe6+dGMr7E20siZHIZQwYqrllxotAhiaej96y6x00b/48rPt42o+SzOSCPbrpsDWvRja40Hwrj0g0q6LZJg== capture-exit@^2.0.0: version "2.0.0" @@ -4115,7 +4120,7 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chokidar@3.4.0, chokidar@^3.3.0, chokidar@^3.4.0: +chokidar@3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== @@ -4149,6 +4154,21 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.3.0, chokidar@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1" + integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -4231,9 +4251,9 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.3.0.tgz#0632239a4b5aa4c958610142c34bb7a651fc8df5" - integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w== + version "2.4.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" + integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== cli-table3@0.5.1: version "0.5.1" @@ -4385,9 +4405,9 @@ color@^3.0.0: color-string "^1.5.2" colorette@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.0.tgz#45306add826d196e8c87236ac05d797f25982e63" - integrity sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw== + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== colors@^1.1.2: version "1.4.0" @@ -4478,7 +4498,7 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -connect-mongo@^3.2.0: +connect-mongo@*, connect-mongo@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/connect-mongo/-/connect-mongo-3.2.0.tgz#20f776c7f2a9d8144fc76cfdcbf33edb05eb4d52" integrity sha512-0Mx88079Z20CG909wCFlR3UxhMYGg6Ibn1hkIje1hwsqOLWtL9HJV+XD0DAjUvQScK6WqY/FA8tSVQM9rR64Rw== @@ -5044,13 +5064,6 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decamelize@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-3.2.0.tgz#84b8e8f4f8c579f938e35e2cc7024907e0090851" - integrity sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw== - dependencies: - xregexp "^4.2.4" - decimal.js@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" @@ -5436,10 +5449,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.483: - version "1.3.488" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.488.tgz#9226229f5fbc825959210e81e0bb3e63035d1c06" - integrity sha512-NReBdOugu1yl8ly+0VDtiQ6Yw/1sLjnvflWq0gvY1nfUXU2PbA+1XAVuEb7ModnwL/MfUPjby7e4pAFnSHiy6Q== +electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.488: + version "1.3.506" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.506.tgz#74bd3e1fb31285b3247f165d4958da74d70ae4e4" + integrity sha512-k0PHtv4gD6KJu1k6lp8pvQOe12uZriOwS2x66Vnxkq0NOBucsNrItOj/ehomvcZ3S4K1ueqUCv+fsLhXBs6Zyw== elliptic@^6.0.0, elliptic@^6.5.2: version "6.5.3" @@ -5487,9 +5500,9 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: once "^1.4.0" enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d" - integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== dependencies: graceful-fs "^4.1.2" memory-fs "^0.5.0" @@ -5597,9 +5610,9 @@ es6-weak-map@^2.0.2: es6-symbol "^3.1.1" escalade@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.1.tgz#52568a77443f6927cd0ab9c73129137533c965ed" - integrity sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" + integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== escape-html@~1.0.3: version "1.0.3" @@ -5798,14 +5811,14 @@ eslint-utils@^1.4.3: dependencies: eslint-visitor-keys "^1.1.0" -eslint-utils@^2.0.0: +eslint-utils@^2.0.0, eslint-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0: +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== @@ -5854,9 +5867,9 @@ eslint@^6.6.0: v8-compile-cache "^2.0.3" eslint@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.4.0.tgz#4e35a2697e6c1972f9d6ef2b690ad319f80f206f" - integrity sha512-gU+lxhlPHu45H3JkEGgYhWhkR9wLHHEXC9FbWFnTlEkbKyZKWgWRLgf61E8zWmBuI6g5xKBph9ltg3NtZMVF8g== + version "7.5.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.5.0.tgz#9ecbfad62216d223b82ac9ffea7ef3444671d135" + integrity sha512-vlUP10xse9sWt9SGRtcr1LAC67BENcQMFeV+w5EvLEoFe3xJ8cF1Skd0msziRx/VMC+72B4DxreCE+OR12OA6Q== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -5866,9 +5879,9 @@ eslint@^7.4.0: doctrine "^3.0.0" enquirer "^2.3.5" eslint-scope "^5.1.0" - eslint-utils "^2.0.0" - eslint-visitor-keys "^1.2.0" - espree "^7.1.0" + eslint-utils "^2.1.0" + eslint-visitor-keys "^1.3.0" + espree "^7.2.0" esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" @@ -5882,7 +5895,7 @@ eslint@^7.4.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.14" + lodash "^4.17.19" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -5904,14 +5917,14 @@ espree@^6.1.2: acorn-jsx "^5.2.0" eslint-visitor-keys "^1.1.0" -espree@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.1.0.tgz#a9c7f18a752056735bf1ba14cb1b70adc3a5ce1c" - integrity sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw== +espree@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.2.0.tgz#1c263d5b513dbad0ac30c4991b93ac354e948d69" + integrity sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g== dependencies: - acorn "^7.2.0" + acorn "^7.3.1" acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.2.0" + eslint-visitor-keys "^1.3.0" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" @@ -5966,9 +5979,9 @@ eventemitter3@^4.0.0: integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== events@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" - integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== eventsource@^1.0.7: version "1.0.7" @@ -6021,9 +6034,9 @@ execa@^1.0.0: strip-eof "^1.0.0" execa@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" - integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q== + version "4.0.3" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2" + integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" @@ -7192,9 +7205,9 @@ human-signals@^1.1.1: integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== hyphenate-style-name@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" - integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== i18next-sync-fs-backend@^1.1.1: version "1.1.1" @@ -7431,9 +7444,9 @@ inquirer@7.2.0: through "^2.3.6" inquirer@^7.0.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.0.tgz#aa3e7cb0c18a410c3c16cdd2bc9dcbe83c4d333e" - integrity sha512-K+LZp6L/6eE5swqIcVXrxl21aGDU4S50gKH0/d96OMQnSBCyGyZl/oZhbkVmdp5sBoINHd4xZvFSARh2dk6DWA== + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.0" @@ -7441,7 +7454,7 @@ inquirer@^7.0.0: cli-width "^3.0.0" external-editor "^3.0.3" figures "^3.0.0" - lodash "^4.17.15" + lodash "^4.17.19" mute-stream "0.0.8" run-async "^2.4.0" rxjs "^6.6.0" @@ -7832,7 +7845,7 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.1.1: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -8844,9 +8857,9 @@ jsdom@^14.1.0: xml-name-validator "^3.0.0" jsdom@^16.2.2: - version "16.2.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.2.2.tgz#76f2f7541646beb46a938f5dc476b88705bedf2b" - integrity sha512-pDFQbcYtKBHxRaP55zGXCJWgFHkDAYbKcsXEK/3Icu9nKYZkutUXfLBwbD+09XDutkYSHcgfQLZ0qvpAAm9mvg== + version "16.3.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.3.0.tgz#75690b7dac36c67be49c336dcd7219bbbed0810c" + integrity sha512-zggeX5UuEknpdZzv15+MS1dPYG0J/TftiiNunOeNxSl3qr8Z6cIlQpN0IdJa44z9aFxZRIVqRncvEhQ7X5DtZg== dependencies: abab "^2.0.3" acorn "^7.1.1" @@ -8868,7 +8881,7 @@ jsdom@^16.2.2: tough-cookie "^3.0.1" w3c-hr-time "^1.0.2" w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.0.0" + webidl-conversions "^6.1.0" whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" @@ -9408,9 +9421,9 @@ luxon@^1.24.1: integrity sha512-CgnIMKAWT0ghcuWFfCWBnWGOddM0zu6c4wZAWmD0NN7MZTnro0+833DF6tJep+xlxRPg4KtsYEHYLfTMBQKwYg== macos-release@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.0.tgz#837b39fc01785c3584f103c5599e0f0c8068b49e" - integrity sha512-ko6deozZYiAkqa/0gmcsz+p4jSy3gY7/ZsCEokPaYd8k+6/aXGkiTgr61+Owup7Sf+xjqW8u2ElhoM9SEcEfuA== + version "2.4.1" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.1.tgz#64033d0ec6a5e6375155a74b1a1eba8e509820ac" + integrity sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg== magic-string@0.25.7: version "0.25.7" @@ -9757,9 +9770,9 @@ mississippi@^3.0.0: through2 "^2.0.0" mitt@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.0.1.tgz#9e8a075b4daae82dd91aac155a0ece40ca7cb393" - integrity sha512-FhuJY+tYHLnPcBHQhbUFzscD5512HumCPE4URXZUgPi3IvOJi4Xva5IIgy3xX56GqCmw++MAm5UURG6kDBYTdg== + version "2.1.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230" + integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg== mixin-deep@^1.2.0: version "1.3.2" @@ -9864,9 +9877,9 @@ mongoose-legacy-pluralize@1.0.2: integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== mongoose@^5.9.24: - version "5.9.24" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.9.24.tgz#ba7f95529da8fa2160d9b4d708b3fe9856c56636" - integrity sha512-uxTLy/ExYmOfKvvbjn1PHbjSJg0SQzff+dW6jbnywtbBcfPRC/3etnG9hPv6KJe/5TFZQGxCyiSezkqa0+iJAQ== + version "5.9.25" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.9.25.tgz#620da737ec9a667f84404ad4f35bb60338dd0b4b" + integrity sha512-vz/DqJ3mrHqEIlfRbKmDZ9TzQ1a0hCtSQpjHScIxr4rEtLs0tjsXDeEWcJ/vEEc3oLfP6vRx9V+uYSprXDUvFQ== dependencies: bson "^1.1.4" kareem "2.3.1" @@ -9988,9 +10001,9 @@ negotiator@0.6.2: integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== neo-async@^2.5.0, neo-async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== nestjs-typegoose@^7.1.28: version "7.1.28" @@ -10091,21 +10104,21 @@ node-notifier@^5.4.2: which "^1.3.0" node-notifier@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-7.0.1.tgz#a355e33e6bebacef9bf8562689aed0f4230ca6f9" - integrity sha512-VkzhierE7DBmQEElhTGJIoiZa1oqRijOtgOlsXg32KrJRXsPy0NXFBqWGW/wTswnJlDCs5viRYaqWguqzsKcmg== + version "7.0.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-7.0.2.tgz#3a70b1b70aca5e919d0b1b022530697466d9c675" + integrity sha512-ux+n4hPVETuTL8+daJXTOC6uKLgMsl1RYfFv7DKRzyvzBapqco0rZZ9g72ZN8VS6V+gvNYHYa/ofcCY8fkJWsA== dependencies: growly "^1.3.0" - is-wsl "^2.1.1" - semver "^7.2.1" + is-wsl "^2.2.0" + semver "^7.3.2" shellwords "^0.1.1" - uuid "^7.0.3" + uuid "^8.2.0" which "^2.0.2" node-releases@^1.1.52, node-releases@^1.1.58: - version "1.1.58" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" - integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg== + version "1.1.60" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" + integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== nodemailer@^6.4.10: version "6.4.10" @@ -10346,9 +10359,9 @@ onetime@^5.1.0: mimic-fn "^2.1.0" open@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/open/-/open-7.0.4.tgz#c28a9d315e5c98340bf979fdcb2e58664aa10d83" - integrity sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ== + version "7.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.1.0.tgz#68865f7d3cb238520fa1225a63cf28bcf8368a1c" + integrity sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA== dependencies: is-docker "^2.0.0" is-wsl "^2.1.1" @@ -10617,9 +10630,9 @@ parse-json@^4.0.0: json-parse-better-errors "^1.0.1" parse-json@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" - integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== + version "5.0.1" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.1.tgz#7cfe35c1ccd641bce3981467e6c2ece61b3b3878" + integrity sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ== dependencies: "@babel/code-frame" "^7.0.0" error-ex "^1.3.1" @@ -10885,9 +10898,9 @@ popper.js@1.16.1-lts: integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== portfinder@^1.0.25: - version "1.0.26" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" - integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== + version "1.0.27" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.27.tgz#a41333c116b5e5f3d380f9745ac2f35084c4c758" + integrity sha512-bJ3U3MThKnyJ9Dx1Idtm5pQmxXqw08+XOHhi/Lie8OF1OlhVaBFhsntAIhkZYjfDcCzszSr0w1yCbccThhzgxQ== dependencies: async "^2.6.2" debug "^3.1.1" @@ -12336,9 +12349,9 @@ regenerator-runtime@^0.11.0: integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== regenerator-transform@^0.14.2: version "0.14.5" @@ -12438,19 +12451,19 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -request-promise-core@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" - integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== +request-promise-core@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" + integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== dependencies: - lodash "^4.17.15" + lodash "^4.17.19" request-promise-native@^1.0.5, request-promise-native@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" - integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== + version "1.0.9" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" + integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== dependencies: - request-promise-core "1.1.3" + request-promise-core "1.1.4" stealthy-require "^1.1.1" tough-cookie "^2.3.3" @@ -13514,9 +13527,9 @@ strip-final-newline@^2.0.0: integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-json-comments@^3.0.1, strip-json-comments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" - integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@0.23.1: version "0.23.1" @@ -13625,9 +13638,9 @@ svgo@^1.0.0, svgo@^1.2.2: util.promisify "~1.0.0" swagger-ui-dist@^3.18.1: - version "3.28.0" - resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.28.0.tgz#7c30ece92f815c1f34de3d394e12983e97f3d421" - integrity sha512-aPkfTzPv9djSiZI1NUkWr5HynCUsH+jaJ0WSx+/t19wq7MMGg9clHm9nGoIpAtqml1G51ofI+I75Ym72pukzFg== + version "3.30.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.30.2.tgz#f3171c81d23709e834506d13bf9bba7ac4883abf" + integrity sha512-hAu/ig5N8i0trXXbrC7rwbXV4DhpEAsZhYXDs1305OjmDgjGC0thINbb0197idy3Pp+B6w7u426SUM43GAP7qw== swagger-ui-express@^4.1.4: version "4.1.4" @@ -13677,9 +13690,9 @@ tar-fs@^2.0.0: tar-stream "^2.0.0" tar-stream@^2.0.0, tar-stream@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" - integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== + version "2.1.3" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" + integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== dependencies: bl "^4.0.1" end-of-stream "^1.4.1" @@ -13929,9 +13942,9 @@ tree-kill@1.2.2: integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== ts-jest@^26.1.2: - version "26.1.2" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.2.tgz#dd2e832ffae9cb803361483b6a3010a6413dc475" - integrity sha512-V4SyBDO9gOdEh+AF4KtXJeP+EeI4PkOrxcA8ptl4o8nCXUVM5Gg/8ngGKneS5BsZaR9DXVQNqj9k+iqGAnpGow== + version "26.1.3" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.3.tgz#aac928a05fdf13e3e6dfbc8caec3847442667894" + integrity sha512-beUTSvuqR9SmKQEylewqJdnXWMVGJRFqSz2M8wKJe7GBMmLZ5zw6XXKSJckbHNMxn+zdB3guN2eOucSw2gBMnw== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -14302,7 +14315,7 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@8.2.0: +uuid@8.2.0, uuid@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.2.0.tgz#cb10dd6b118e2dada7d0cd9730ba7417c93d920e" integrity sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q== @@ -14312,7 +14325,7 @@ uuid@^3.0.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^7.0.2, uuid@^7.0.3: +uuid@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== @@ -14455,7 +14468,7 @@ webidl-conversions@^5.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== -webidl-conversions@^6.0.0: +webidl-conversions@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== @@ -14628,9 +14641,9 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: iconv-lite "0.4.24" whatwg-fetch@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.1.0.tgz#49d630cdfa308dba7f2819d49d09364f540dbcc6" - integrity sha512-pgmbsVWKpH9GxLXZmtdowDIqtb/rvPyjjQv3z9wLcmgWKFHilKnZD3ldgrOlwJoPGOUluQsRPWd52yVkPfmI1A== + version "3.2.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz#8e134f701f0a4ab5fda82626f113e2b647fd16dc" + integrity sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w== whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" @@ -14926,9 +14939,9 @@ ws@^6.1.2, ws@^6.2.1: async-limiter "~1.0.0" ws@^7.2.3: - version "7.3.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" - integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== + version "7.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== xml-name-validator@^3.0.0: version "3.0.0" @@ -14945,7 +14958,7 @@ xmlchars@^2.1.1, xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xregexp@^4.2.4, xregexp@^4.3.0: +xregexp@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" integrity sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g== @@ -15036,12 +15049,12 @@ yargs@^13.3.0: yargs-parser "^13.1.2" yargs@^15.3.1: - version "15.4.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.0.tgz#53949fb768309bac1843de9b17b80051e9805ec2" - integrity sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw== + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" - decamelize "^3.2.0" + decamelize "^1.2.0" find-up "^4.1.0" get-caller-file "^2.0.1" require-directory "^2.1.1" From 63617335ee96e4f510ffb49fa7c9404d7cfcf67f Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jul 2020 18:47:40 +0200 Subject: [PATCH 12/62] Fix more issues with the @types/luxon update. This also extends the eslint scripts in the server package.json to include the test/ folder. --- .../components/DateOfTutorialSelection.tsx | 4 +- .../src/components/date-picker/DatePicker.tsx | 10 ++- .../src/components/forms/ScheinExamForm.tsx | 4 +- client/src/components/forms/TutorialForm.tsx | 20 +++--- .../components/FormikMultipleDatesPicker.tsx | 5 +- .../select-interval/SelectInterval.tsx | 6 +- client/src/util/helperFunctions.ts | 8 ++- .../src/view/attendance/AttendanceManager.tsx | 4 +- .../generate-tutorials/GenerateTutorials.tsx | 14 ++-- .../excluded-dates/FormikExcludedDates.tsx | 2 +- .../ScheinExamManagement.tsx | 2 +- .../student-info/StudentInfo.tsx | 4 +- .../SubstituteManagement.context.tsx | 9 ++- .../SubstituteManagement.tsx | 10 +-- .../components/DateBox.tsx | 2 +- .../tutorialmanagement/TutorialManagement.tsx | 6 +- .../components/TutorialTableRow.tsx | 2 +- server/package.json | 6 +- .../module/student/student.service.spec.ts | 2 +- .../module/tutorial/tutorial.service.spec.ts | 70 +++++++++---------- server/test/app.e2e-spec.ts | 3 +- server/test/helpers/test.assertExercises.ts | 4 +- server/test/helpers/test.create-mock-model.ts | 11 +-- server/test/helpers/test.module.ts | 4 +- server/test/helpers/test.provider.ts | 4 +- server/test/mocks/documents.mock.helpers.ts | 1 - server/test/mocks/documents.mock.ts | 4 +- 27 files changed, 115 insertions(+), 106 deletions(-) diff --git a/client/src/components/DateOfTutorialSelection.tsx b/client/src/components/DateOfTutorialSelection.tsx index 789e69538..ceade0a30 100644 --- a/client/src/components/DateOfTutorialSelection.tsx +++ b/client/src/components/DateOfTutorialSelection.tsx @@ -42,8 +42,8 @@ function DateOfTutorialSelection({ className={className} FormControlProps={other} items={availableDates} - itemToString={(date) => date.toLocaleString(DateTime.DATE_MED)} - itemToValue={(date) => date.toISODate()} + itemToString={(date) => date.toLocaleString(DateTime.DATE_MED) ?? 'DATE_NOTE_PARSEABLE'} + itemToValue={(date) => date.toISODate() ?? 'DATE_NOTE_PARSEABLE'} /> ); } diff --git a/client/src/components/date-picker/DatePicker.tsx b/client/src/components/date-picker/DatePicker.tsx index fcadf036f..02bc03833 100644 --- a/client/src/components/date-picker/DatePicker.tsx +++ b/client/src/components/date-picker/DatePicker.tsx @@ -1,6 +1,6 @@ -import React from 'react'; import { KeyboardDatePicker, KeyboardDatePickerProps } from '@material-ui/pickers'; -import { DateTimeFormatOptions, DateTime } from 'luxon'; +import { DateTime, DateTimeFormatOptions } from 'luxon'; +import React from 'react'; const DATE_FORMAT: DateTimeFormatOptions = { weekday: 'short', @@ -13,11 +13,9 @@ export type CustomDatePickerProps = KeyboardDatePickerProps; function CustomDatePicker(props: CustomDatePickerProps): JSX.Element { const labelFunc = (date: DateTime | null, invalidLabel: string) => { - if (!date || !date.isValid) { - return invalidLabel; - } + const label = date?.toLocaleString(DATE_FORMAT); - return date.toLocaleString(DATE_FORMAT); + return !!label ? label : invalidLabel; }; return ( diff --git a/client/src/components/forms/ScheinExamForm.tsx b/client/src/components/forms/ScheinExamForm.tsx index 39dc01192..6ef14be90 100644 --- a/client/src/components/forms/ScheinExamForm.tsx +++ b/client/src/components/forms/ScheinExamForm.tsx @@ -36,7 +36,7 @@ export function getInitialExamFormState( scheinExamNo: exam.scheinExamNo.toString(), exercises, percentageNeeded: exam.percentageNeeded, - date: exam.date.toISODate(), + date: exam.date.toISODate() ?? '', }; } @@ -48,7 +48,7 @@ export function getInitialExamFormState( return { scheinExamNo: (lastScheinExamNo + 1).toString(), exercises: [], - date: DateTime.local().toISODate(), + date: DateTime.local().toISODate() ?? '', percentageNeeded: 0.5, }; } diff --git a/client/src/components/forms/TutorialForm.tsx b/client/src/components/forms/TutorialForm.tsx index 0e835fdff..846e1c3ba 100644 --- a/client/src/components/forms/TutorialForm.tsx +++ b/client/src/components/forms/TutorialForm.tsx @@ -7,13 +7,13 @@ import { IUser } from 'shared/model/User'; import * as Yup from 'yup'; import { Tutorial } from '../../model/Tutorial'; import { FormikSubmitCallback } from '../../types'; +import { compareDateTimes } from '../../util/helperFunctions'; import FormikDatePicker from './components/FormikDatePicker'; import FormikMultipleDatesPicker from './components/FormikMultipleDatesPicker'; import FormikSelect from './components/FormikSelect'; import FormikTextField from './components/FormikTextField'; import FormikTimePicker from './components/FormikTimePicker'; import FormikBaseForm, { CommonlyUsedFormProps, FormikBaseFormProps } from './FormikBaseForm'; -import { compareDateTimes } from '../../util/helperFunctions'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -114,8 +114,8 @@ function getAllWeeklyDatesBetween(startDate: DateTime, endDate: DateTime): DateT } export function getInitialTutorialFormValues(tutorial?: Tutorial): TutorialFormState { - const startDate = DateTime.local().toISODate(); - const endDate = DateTime.local().toISODate(); + const startDate = DateTime.local().toISODate() ?? ''; + const endDate = DateTime.local().toISODate() ?? ''; if (!tutorial) { return { @@ -123,8 +123,8 @@ export function getInitialTutorialFormValues(tutorial?: Tutorial): TutorialFormS tutor: '', startDate, endDate, - startTime: DateTime.local().toISO(), - endTime: DateTime.local().toISO(), + startTime: DateTime.local().toISO() ?? '', + endTime: DateTime.local().toISO() ?? '', correctors: [], selectedDates: [], }; @@ -135,14 +135,14 @@ export function getInitialTutorialFormValues(tutorial?: Tutorial): TutorialFormS return { slot: tutorial.slot, tutor: tutorial.tutor ? tutorial.tutor.id : '', - startDate: sortedDates[0] ? sortedDates[0].toISODate() : startDate, + startDate: sortedDates[0] ? sortedDates[0].toISODate() ?? '' : startDate, endDate: sortedDates[sortedDates.length - 1] - ? sortedDates[sortedDates.length - 1].toISODate() + ? sortedDates[sortedDates.length - 1].toISODate() ?? '' : endDate, - startTime: tutorial.startTime.toISO(), - endTime: tutorial.endTime.toISO(), + startTime: tutorial.startTime.toISO() ?? '', + endTime: tutorial.endTime.toISO() ?? '', correctors: tutorial.correctors.map((c) => c.id), - selectedDates: tutorial.dates.map((DateTime) => DateTime.toISODate()), + selectedDates: tutorial.dates.map((DateTime) => DateTime.toISODate() ?? '').filter(Boolean), }; } diff --git a/client/src/components/forms/components/FormikMultipleDatesPicker.tsx b/client/src/components/forms/components/FormikMultipleDatesPicker.tsx index 167ecaec0..811ba16c6 100644 --- a/client/src/components/forms/components/FormikMultipleDatesPicker.tsx +++ b/client/src/components/forms/components/FormikMultipleDatesPicker.tsx @@ -96,7 +96,7 @@ function isStringArray(array: unknown): array is string[] { } export function getDateString(date: DateTime): string { - return date.toISODate(); + return date.toISODate() ?? 'DATE_NOTE_PARSEABLE'; } function FormikMultipleDatesPicker({ @@ -203,7 +203,8 @@ function FormikMultipleDatesPicker({ ((date) => ({ dateValueString: date, - dateDisplayString: DateTime.fromISO(date).toLocaleString(DateTime.DATE_MED), + dateDisplayString: + DateTime.fromISO(date).toLocaleString(DateTime.DATE_MED) ?? 'DATE_NOTE_PARSEABLE', }))} onDateClicked={onDateInListClicked(form.values[name], arrayHelpers)} /> diff --git a/client/src/components/select-interval/SelectInterval.tsx b/client/src/components/select-interval/SelectInterval.tsx index 18e3b7bfe..21cb3deba 100644 --- a/client/src/components/select-interval/SelectInterval.tsx +++ b/client/src/components/select-interval/SelectInterval.tsx @@ -172,11 +172,9 @@ function SelectInterval({ }; const labelFunc = (date: DateTime | null, invalidLabel: string) => { - if (!date || !date.isValid) { - return invalidLabel; - } + const label = date?.toLocaleString(format.display); - return date.toLocaleString(format.display); + return label ?? invalidLabel; }; const handleStartChanged = (date: DateTime | null) => { diff --git a/client/src/util/helperFunctions.ts b/client/src/util/helperFunctions.ts index 6d912a9ce..3c78fef21 100644 --- a/client/src/util/helperFunctions.ts +++ b/client/src/util/helperFunctions.ts @@ -5,7 +5,13 @@ export function compareDateTimes(a: DateTime, b: DateTime): number { } export function parseDateToMapKey(date: DateTime): string { - return date.toISODate(); + const dateKey = date.toISODate(); + + if (!dateKey) { + throw new Error(`DATE_NOT_PARSABLE: {date}`); + } + + return dateKey; } export function saveBlob(blob: Blob, filename: string): void { diff --git a/client/src/view/attendance/AttendanceManager.tsx b/client/src/view/attendance/AttendanceManager.tsx index 5d9b8833d..57e6ebd7a 100644 --- a/client/src/view/attendance/AttendanceManager.tsx +++ b/client/src/view/attendance/AttendanceManager.tsx @@ -196,7 +196,7 @@ function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element const attendance: IAttendance | undefined = student.getAttendance(date); const attendanceDTO: IAttendanceDTO = { state: attendanceState, - date: date.toISODate(), + date: date.toISODate() ?? 'DATE_NOTE_PARSEABLE', note: attendance ? attendance.note : '', }; @@ -217,7 +217,7 @@ function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element const attendance: IAttendance | undefined = student.getAttendance(date); const attendanceDTO: IAttendanceDTO = { state: attendance ? attendance.state : undefined, - date: date.toISODate(), + date: date.toISODate() ?? 'DATE_NOTE_PARSEABLE', note, }; diff --git a/client/src/view/generate-tutorials/GenerateTutorials.tsx b/client/src/view/generate-tutorials/GenerateTutorials.tsx index 81b96893e..cbe270b93 100644 --- a/client/src/view/generate-tutorials/GenerateTutorials.tsx +++ b/client/src/view/generate-tutorials/GenerateTutorials.tsx @@ -68,16 +68,16 @@ function generateDTOFromValues(values: FormState): ITutorialGenerationDTO { const { startDate, endDate, excludedDates, weekdays, prefixes } = values; return { - firstDay: DateTime.fromISO(startDate).toISODate(), - lastDay: DateTime.fromISO(endDate).toISODate(), + firstDay: DateTime.fromISO(startDate).toISODate() ?? 'DATE_NOTE_PARSEABLE', + lastDay: DateTime.fromISO(endDate).toISODate() ?? 'DATE_NOTE_PARSEABLE', excludedDates: excludedDates.map((ex) => { if (ex instanceof DateTime) { - return { date: ex.toISODate() }; + return { date: ex.toISODate() ?? 'DATE_NOTE_PARSEABLE' }; } else if (ex instanceof Interval) { if (ex.start.day === ex.end.day) { - return { date: ex.start.toISODate() }; + return { date: ex.start.toISODate() ?? 'DATE_NOTE_PARSEABLE' }; } else { - return { interval: ex.toISODate() }; + return { interval: ex.toISODate() ?? 'DATE_NOTE_PARSEABLE' }; } } else { throw new Error('Given excluded date is neither a DateTime nor an Interval'); @@ -169,8 +169,8 @@ function GenerateTutorials(): JSX.Element { const history = useHistory(); const initialValues: FormState = { - startDate: DateTime.local().toISODate(), - endDate: DateTime.local().plus({ days: 1 }).toISODate(), + startDate: DateTime.local().toISODate() ?? '', + endDate: DateTime.local().plus({ days: 1 }).toISODate() ?? '', excludedDates: [], weekdays: {}, prefixes: initialPrefixes, diff --git a/client/src/view/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx b/client/src/view/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx index 59f95281a..2f91ebd30 100644 --- a/client/src/view/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx +++ b/client/src/view/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx @@ -113,7 +113,7 @@ function FormikExcludedDates({ name, ...props }: Props): JSX.Element { > {value.map((val, idx) => ( { setDialogState({ diff --git a/client/src/view/scheinexam-management/ScheinExamManagement.tsx b/client/src/view/scheinexam-management/ScheinExamManagement.tsx index 945ea0882..228093d25 100644 --- a/client/src/view/scheinexam-management/ScheinExamManagement.tsx +++ b/client/src/view/scheinexam-management/ScheinExamManagement.tsx @@ -46,7 +46,7 @@ function generateScheinExamDTO(values: ScheinExamFormState): IScheinexamDTO { scheinExamNo: Number.parseFloat(values.scheinExamNo), exercises: convertFormExercisesToDTOs(values.exercises), percentageNeeded: values.percentageNeeded, - date: date.toISODate(), + date: date.toISODate() ?? 'DATE_NOTE_PARSEABLE', }; } diff --git a/client/src/view/studentmanagement/student-info/StudentInfo.tsx b/client/src/view/studentmanagement/student-info/StudentInfo.tsx index ebd620f40..99a887637 100644 --- a/client/src/view/studentmanagement/student-info/StudentInfo.tsx +++ b/client/src/view/studentmanagement/student-info/StudentInfo.tsx @@ -122,7 +122,7 @@ function StudentInfo(): JSX.Element { const attendance: IAttendance | undefined = student.getAttendance(date); const attendanceDTO: IAttendanceDTO = { state: attendanceState, - date: date.toISODate(), + date: date.toISODate() ?? 'DATE_NOTE_PARSEABLE', note: attendance?.note ?? '', }; @@ -144,7 +144,7 @@ function StudentInfo(): JSX.Element { const attendance: IAttendance | undefined = student.getAttendance(date); const attendanceDTO: IAttendanceDTO = { state: attendance?.state, - date: date.toISODate(), + date: date.toISODate() ?? 'DATE_NOTE_PARSEABLE', note, }; diff --git a/client/src/view/tutorial-substitutes/SubstituteManagement.context.tsx b/client/src/view/tutorial-substitutes/SubstituteManagement.context.tsx index 482f2d517..3ef55398b 100644 --- a/client/src/view/tutorial-substitutes/SubstituteManagement.context.tsx +++ b/client/src/view/tutorial-substitutes/SubstituteManagement.context.tsx @@ -86,19 +86,22 @@ function SubstituteManagementContextProvider({ const getSelectedSubstitute = useCallback( (date: DateTime) => { - return selectedSubstitutes.get(date.toISODate()); + return selectedSubstitutes.get(date.toISODate() ?? 'DATE_NOTE_PARSEABLE'); }, [selectedSubstitutes] ); const setSelectedSubstitute = useCallback( (tutor: NamedElement, date: DateTime) => { - updateSubstituteMap(new Map(selectedSubstitutes.set(date.toISODate(), tutor))); + const dateKey = date.toISODate(); + if (dateKey) { + updateSubstituteMap(new Map(selectedSubstitutes.set(dateKey, tutor))); + } }, [selectedSubstitutes] ); const removeSelectedSubstitute = useCallback( (date: DateTime) => { - selectedSubstitutes.delete(date.toISODate()); + selectedSubstitutes.delete(date.toISODate() ?? 'DATE_NOTE_PARSEABLE'); updateSubstituteMap(new Map(selectedSubstitutes)); }, [selectedSubstitutes] diff --git a/client/src/view/tutorial-substitutes/SubstituteManagement.tsx b/client/src/view/tutorial-substitutes/SubstituteManagement.tsx index 9d695abd5..0cb824210 100644 --- a/client/src/view/tutorial-substitutes/SubstituteManagement.tsx +++ b/client/src/view/tutorial-substitutes/SubstituteManagement.tsx @@ -54,14 +54,16 @@ function SubstituteManagementContent(): JSX.Element { if (!!substitute) { const datesOfSubstitute = datesWithSubst.get(substitute.id) ?? []; - datesOfSubstitute.push(date.toISODate()); - datesWithSubst.set(substitute.id, datesOfSubstitute); + datesOfSubstitute.push(date.toISODate() ?? ''); + datesWithSubst.set(substitute.id, datesOfSubstitute.filter(Boolean)); } else { - datesWithoutSubst.push(date.toISODate()); + datesWithoutSubst.push(date.toISODate() ?? ''); } }); - const substituteDTOs: ISubstituteDTO[] = [{ tutorId: undefined, dates: datesWithoutSubst }]; + const substituteDTOs: ISubstituteDTO[] = [ + { tutorId: undefined, dates: datesWithoutSubst.filter(Boolean) }, + ]; datesWithSubst.forEach((dates, tutorId) => substituteDTOs.push({ tutorId, dates })); diff --git a/client/src/view/tutorial-substitutes/components/DateBox.tsx b/client/src/view/tutorial-substitutes/components/DateBox.tsx index d9fedbcb1..92bd066a7 100644 --- a/client/src/view/tutorial-substitutes/components/DateBox.tsx +++ b/client/src/view/tutorial-substitutes/components/DateBox.tsx @@ -122,7 +122,7 @@ function DateBox(): JSX.Element { {tutorial.value && datesToShow.map((date) => ( DateTime.fromISO(date)) - .map((date) => date.toISODate()); + .map((date) => date.toISODate() ?? 'DATE_NOTE_PARSEABLE'); return { slot, tutorId: tutor, dates, - startTime: startTime.toISOTime(), - endTime: endTime.toISOTime(), + startTime: startTime.toISOTime() ?? 'DATE_NOTE_PARSEABLE', + endTime: endTime.toISOTime() ?? 'DATE_NOTE_PARSEABLE', correctorIds: correctors, }; } diff --git a/client/src/view/tutorialmanagement/components/TutorialTableRow.tsx b/client/src/view/tutorialmanagement/components/TutorialTableRow.tsx index bb3306b73..81d372646 100644 --- a/client/src/view/tutorialmanagement/components/TutorialTableRow.tsx +++ b/client/src/view/tutorialmanagement/components/TutorialTableRow.tsx @@ -107,7 +107,7 @@ function TutorialTableRow({
{substitutes.map((sub) => ( diff --git a/server/package.json b/server/package.json index 7898005fb..11711bd89 100644 --- a/server/package.json +++ b/server/package.json @@ -7,11 +7,11 @@ "scripts": { "prebuild": "rimraf dist", "build": "nest build", - "format": "eslint --fix \"src/**/*.ts?(x)\"", + "format": "eslint --fix \"src/**/*.ts?(x)\" \"test/**/*.ts?(x)\"", "lint": "yarn eslint:check", - "eslint:check": "eslint \"src/**/*.ts?(x)\"", + "eslint:check": "eslint \"src/**/*.ts?(x)\" \"test/**/*.ts?(x)\"", "prettier:check": "prettier --check \"src/**/*.ts?(x)\"", - "prettier:format": "prettier --write --loglevel warn \"(src|test)/**/*.ts?(x)\"", + "prettier:format": "prettier --write --loglevel warn \"src/**/*.ts?(x)\"", "ts:check": "tsc --project . --noEmit --incremental false", "ts:check:watch": "tsc --project . --noEmit --incremental false -w", "start": "env-cmd cross-env NODE_ENV=development nest start --debug --watch", diff --git a/server/src/module/student/student.service.spec.ts b/server/src/module/student/student.service.spec.ts index 3fc040cc3..021423b13 100644 --- a/server/src/module/student/student.service.spec.ts +++ b/server/src/module/student/student.service.spec.ts @@ -631,7 +631,7 @@ describe('StudentService', () => { const scheinexamDTO: ScheinexamDTO = { scheinExamNo: 17, percentageNeeded: 0.5, - date: DateTime.fromISO('2020-02-08').toISODate(), + date: DateTime.fromISO('2020-02-08').toISODate() ?? 'DATE_NOTE_PARSEABLE', exercises: [ { exName: '1', diff --git a/server/src/module/tutorial/tutorial.service.spec.ts b/server/src/module/tutorial/tutorial.service.spec.ts index 65e184968..4ee47a840 100644 --- a/server/src/module/tutorial/tutorial.service.spec.ts +++ b/server/src/module/tutorial/tutorial.service.spec.ts @@ -153,7 +153,7 @@ function getDatesInInterval( } if (!isExcluded) { - datesInMap.push(current.toISODate()); + datesInMap.push(current.toISODate() ?? 'DATE_NOTE_PARSABLE'); dates.set(current.weekday, datesInMap); } current = current.plus({ days: 1 }); @@ -263,8 +263,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -280,8 +280,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: 'Tutorial 3', tutorId: tutorDoc._id, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -297,8 +297,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: correctorDocs.map((corrector) => corrector._id), }; @@ -315,8 +315,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: 'Tutorial 3', tutorId: tutorDoc._id, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: correctorDocs.map((corrector) => corrector._id), }; @@ -332,8 +332,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: 'Tutorial 3', tutorId: tutorDoc._id, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -348,8 +348,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [...correctors, tutorDoc._id], }; @@ -362,8 +362,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: tutorial.slot, tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -375,16 +375,16 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; const createDTO: TutorialDTO = { ...updatedDTO, slot: 'Tutorial Prev', - startTime: DateTime.fromISO('14:00:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('15:30:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('14:00:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('15:30:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings('2020-02-18'), }; @@ -399,8 +399,8 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: tutors[0]._id, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -420,8 +420,8 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -442,8 +442,8 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [correctors[0]._id], }; @@ -463,8 +463,8 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: nonExistingId, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -483,8 +483,8 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [nonExistingId], }; @@ -503,8 +503,8 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: nonTutor._id, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; @@ -523,8 +523,8 @@ describe('TutorialService', () => { const updatedDTO: TutorialDTO = { slot: 'Tutorial 3', tutorId: undefined, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toISOTime() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [nonCorrector._id], }; @@ -544,8 +544,8 @@ describe('TutorialService', () => { const dto: TutorialDTO = { slot: 'Tutorial 3', tutorId: tutorDoc._id, - startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON(), - endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON(), + startTime: DateTime.fromISO('09:45:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', + endTime: DateTime.fromISO('11:15:00', { zone: 'utc' }).toJSON() ?? 'DATE_NOTE_PARSABLE', dates: createDatesForTutorialAsStrings(), correctorIds: [], }; diff --git a/server/test/app.e2e-spec.ts b/server/test/app.e2e-spec.ts index 4c98bf203..272683153 100644 --- a/server/test/app.e2e-spec.ts +++ b/server/test/app.e2e-spec.ts @@ -1,6 +1,5 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import request from 'supertest'; +import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { diff --git a/server/test/helpers/test.assertExercises.ts b/server/test/helpers/test.assertExercises.ts index 9f8742434..e2eaf105b 100644 --- a/server/test/helpers/test.assertExercises.ts +++ b/server/test/helpers/test.assertExercises.ts @@ -46,7 +46,7 @@ function assertSubExercise({ expected, actual }: AssertSubExerciseParams) { * * @param params Must contain an expected ExerciseDocument and an actual Exercise. */ -export function assertExercise({ expected, actual }: AssertExerciseParams) { +export function assertExercise({ expected, actual }: AssertExerciseParams): void { const { subexercises, ...restExpected } = expected; const { subexercises: actualSubexercises, ...restActual } = actual; @@ -70,7 +70,7 @@ export function assertExercise({ expected, actual }: AssertExerciseParams) { * * @param params Must contain a list of expected DTOs and a list with the actual exercises. */ -export function assertExerciseDTOs({ expected, actual }: AssertExerciseDTOsParams) { +export function assertExerciseDTOs({ expected, actual }: AssertExerciseDTOsParams): void { expect(actual.length).toEqual(expected.length); for (let i = 0; i < expected.length; i++) { diff --git a/server/test/helpers/test.create-mock-model.ts b/server/test/helpers/test.create-mock-model.ts index 7b8f9d9a2..a4b7393d3 100644 --- a/server/test/helpers/test.create-mock-model.ts +++ b/server/test/helpers/test.create-mock-model.ts @@ -1,5 +1,5 @@ -import { generateObjectId } from './test.helpers'; import { UserModel } from '../../src/database/models/user.model'; +import { generateObjectId } from './test.helpers'; type MockedModel = M & { _id: string }; @@ -8,9 +8,12 @@ export type MockedUserModel = MockedModel & { decryptFieldsSync: () => void; }; -export function createMockModel(model: M): MockedModel; -export function createMockModel(model: M, additional: T): MockedModel; -export function createMockModel( +export function createMockModel>(model: M): MockedModel; +export function createMockModel>( + model: M, + additional: T +): MockedModel; +export function createMockModel>( model: M, additional?: T ): MockedModel | MockedModel { diff --git a/server/test/helpers/test.module.ts b/server/test/helpers/test.module.ts index 81d94ec52..59f73b90a 100644 --- a/server/test/helpers/test.module.ts +++ b/server/test/helpers/test.module.ts @@ -85,7 +85,7 @@ export class TestModule implements OnApplicationShutdown { @Inject(getConnectionToken()) private readonly connection: Connection ) {} - async reset() { + async reset(): Promise { if (!this.connection) { return; } @@ -97,7 +97,7 @@ export class TestModule implements OnApplicationShutdown { await this.fillCollections(); } - async onApplicationShutdown() { + async onApplicationShutdown(): Promise { if (this.mongodb) { await this.mongodb.stop(); } diff --git a/server/test/helpers/test.provider.ts b/server/test/helpers/test.provider.ts index cb56b23c0..8268fe670 100644 --- a/server/test/helpers/test.provider.ts +++ b/server/test/helpers/test.provider.ts @@ -43,7 +43,7 @@ export class MongooseMockModelProvider { this.documents = documents.map((doc) => this.adjustDocument(doc, additionalProperties)); } - find(conditions?: any): MockedQuery { + find(conditions?: unknown): MockedQuery { if (!conditions) { return new MockedQuery([...this.documents]); } @@ -59,7 +59,7 @@ export class MongooseMockModelProvider { return new MockedQuery(docsToReturn); } - findOne(conditions: any): MockedQuery { + findOne(conditions: unknown): MockedQuery { for (const doc of this.documents) { if (this.checkConditions(doc, conditions)) { return new MockedQuery(doc); diff --git a/server/test/mocks/documents.mock.helpers.ts b/server/test/mocks/documents.mock.helpers.ts index ea583f301..5fee5f593 100644 --- a/server/test/mocks/documents.mock.helpers.ts +++ b/server/test/mocks/documents.mock.helpers.ts @@ -1,5 +1,4 @@ import { NotFoundException } from '@nestjs/common'; -import { DateTime } from 'luxon'; import { UserModel } from '../../src/database/models/user.model'; import { Role } from '../../src/shared/model/Role'; import { MockedModel } from '../helpers/testdocument'; diff --git a/server/test/mocks/documents.mock.ts b/server/test/mocks/documents.mock.ts index 46d2725d5..4c9fc3d83 100644 --- a/server/test/mocks/documents.mock.ts +++ b/server/test/mocks/documents.mock.ts @@ -357,7 +357,7 @@ export const SCHEINCRITERIA_DOCUMENTS: MockedScheincriteriaModel[] = [ }, ]; -function generateFakeDocument(_id: string, additional?: object): any { +function generateFakeDocument(_id: string, additional?: Record): any { return { _id, id: _id, ...additional }; } @@ -398,5 +398,5 @@ export function createDatesForTutorialAsStrings(startISODate: string = '2020-02- dates.push(baseDate.plus({ weeks: i })); } - return dates.map((date) => date.toISODate()); + return dates.map((date) => date.toISODate() ?? 'DATE_NOTE_PARSEABLE'); } From 474da8549ed847f2add8cf5ee7143c96449044ec Mon Sep 17 00:00:00 2001 From: Dudrie Date: Mon, 27 Jul 2020 21:10:12 +0200 Subject: [PATCH 13/62] Add eslint-plugin-react-hooks as eslint plugin. --- .eslintrc.yml | 1 + client/src/hooks/useFetchState.ts | 14 +++++++------- .../components/SelectSubstitute.tsx | 7 +++---- package.json | 1 + yarn.lock | 5 +++++ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 34e15c628..0e2528831 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -9,6 +9,7 @@ plugins: extends: - 'plugin:react/recommended' # Uses the recommended rules from @eslint-plugin-react + - 'plugin:react-hooks/recommended' - 'plugin:@typescript-eslint/recommended' # Uses the recommended rules from the @typescript-eslint/eslint-plugin - 'plugin:import/typescript' - 'prettier/@typescript-eslint' # Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier diff --git a/client/src/hooks/useFetchState.ts b/client/src/hooks/useFetchState.ts index f104af20e..297128f7b 100644 --- a/client/src/hooks/useFetchState.ts +++ b/client/src/hooks/useFetchState.ts @@ -1,25 +1,25 @@ import _ from 'lodash'; import { useCallback, useEffect, useState } from 'react'; -type Arr = readonly unknown[]; +type BaseArrayType = readonly unknown[]; -interface UseFetchStateParamsDelayed { +interface UseFetchStateParamsDelayed { fetchFunction: (...params: P) => Promise; immediate?: false; params?: never; } -interface UseFetchStateParamsImmediate { +interface UseFetchStateParamsImmediate { fetchFunction: (...params: P) => Promise; immediate: true; params: P; } -export type UseFetchStateParams = +export type UseFetchStateParams = | UseFetchStateParamsDelayed | UseFetchStateParamsImmediate; -export interface UseFetchState extends FetchState { +export interface UseFetchState extends FetchState { execute: (...params: P) => Promise; } @@ -29,7 +29,7 @@ export interface FetchState { error?: string; } -export function useFetchState({ +export function useFetchState({ fetchFunction, immediate, params, @@ -55,7 +55,7 @@ export function useFetchState({ useEffect(() => { if (immediate) { if (params) { - // Make a deep equal check to prevent infinite re-executin the callback. + // Make a deep equal check to prevent re-executing the callback an 'infinite' times. if (!_.isEqual(params, prevParams)) { setPrevParams(params); execute(...params); diff --git a/client/src/view/tutorial-substitutes/components/SelectSubstitute.tsx b/client/src/view/tutorial-substitutes/components/SelectSubstitute.tsx index 8cb97b058..9a223601c 100644 --- a/client/src/view/tutorial-substitutes/components/SelectSubstitute.tsx +++ b/client/src/view/tutorial-substitutes/components/SelectSubstitute.tsx @@ -2,7 +2,7 @@ import { Box, Button, Divider, InputProps, TextField, Typography } from '@materi import { createStyles, makeStyles } from '@material-ui/core/styles'; import _ from 'lodash'; import { AccountSearch as SearchIcon } from 'mdi-material-ui'; -import React, { useCallback, useEffect, useReducer } from 'react'; +import React, { useEffect, useReducer, useRef } from 'react'; import { NamedElement } from 'shared/model/Common'; import { getNameOfEntity } from 'shared/util/helpers'; import DateOrIntervalText from '../../../components/DateOrIntervalText'; @@ -113,9 +113,8 @@ function SelectSubstitute(): JSX.Element { dispatch({ type: 'changeTutorial', data: tutorial.value }); }, [tutorial.value]); - const debouncedHandleChange = useCallback( - _.debounce((filterText: string) => dispatch({ type: 'changeFilter', data: filterText }), 250), - [] + const { current: debouncedHandleChange } = useRef( + _.debounce((filterText: string) => dispatch({ type: 'changeFilter', data: filterText }), 250) ); const handleTextChange: InputProps['onChange'] = (e) => { diff --git a/package.json b/package.json index baaf93063..8df7f18d0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-plugin-import": "^2.22.0", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.8", "jest-circus": "^26.1.0", "prettier": "^2.0.5", "ts-node": "^8.10.2", diff --git a/yarn.lock b/yarn.lock index 6f0729033..52c12875d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5753,6 +5753,11 @@ eslint-plugin-react-hooks@^1.6.1: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04" integrity sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA== +eslint-plugin-react-hooks@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.8.tgz#a9b1e3d57475ccd18276882eff3d6cba00da7a56" + integrity sha512-6SSb5AiMCPd8FDJrzah+Z4F44P2CdOaK026cXFV+o/xSRzfOiV1FNFeLl2z6xm3yqWOQEZ5OfVgiec90qV2xrQ== + eslint-plugin-react@7.19.0: version "7.19.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz#6d08f9673628aa69c5559d33489e855d83551666" From 86a9491035aea9577f657abbb02e4fc7e1ba3498 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Tue, 28 Jul 2020 14:13:55 +0200 Subject: [PATCH 14/62] Add endpoint to change the client settings. --- server/src/database/models/settings.model.ts | 23 +++++++++++-- .../model/ApplicationConfiguration.ts | 18 +++-------- .../module/settings/settings.controller.ts | 18 ++++++++--- server/src/module/settings/settings.dto.ts | 13 ++++++++ .../module/settings/settings.service.spec.ts | 32 +++++++++++++++++++ .../src/module/settings/settings.service.ts | 24 +++++++++++--- server/src/shared/model/Settings.ts | 2 +- server/test/helpers/test.module.ts | 3 +- server/test/mocks/documents.mock.ts | 5 +++ 9 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 server/src/module/settings/settings.dto.ts diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index afd0fc9a0..fec43a7c5 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -1,9 +1,11 @@ import { DocumentType, modelOptions, prop } from '@typegoose/typegoose'; import { CollectionName } from '../../helpers/CollectionName'; +import { ClientSettingsDTO } from '../../module/settings/settings.dto'; +import { IClientSettings } from '../../shared/model/Settings'; @modelOptions({ schemaOptions: { collection: CollectionName.SETTINGS } }) export class SettingsModel { - private static get internalDefaults(): ISettings { + private static get internalDefaults(): IClientSettings { return { defaultTeamSize: 2, canTutorExcuseStudents: false }; } @@ -13,14 +15,14 @@ export class SettingsModel { @prop({ required: true }) canTutorExcuseStudents: boolean; - constructor(fields?: Partial) { + constructor(fields?: Partial) { this.defaultTeamSize = fields?.defaultTeamSize ?? SettingsModel.internalDefaults.defaultTeamSize; this.canTutorExcuseStudents = fields?.canTutorExcuseStudents ?? SettingsModel.internalDefaults.canTutorExcuseStudents; } - toDTO(): ISettings { + toDTO(): IClientSettings { const defaultSettings = SettingsModel.internalDefaults; return { @@ -28,6 +30,21 @@ export class SettingsModel { canTutorExcuseStudents: this.canTutorExcuseStudents ?? defaultSettings.canTutorExcuseStudents, }; } + + /** + * Changes this settings document to use the newly provided settings. + * + * If a setting is not part of the provided `SettingsDTO` the old value previously saved in this document gets used. + * + * @param dto DTO with the new settings information. + */ + assignDTO(dto: ClientSettingsDTO): void { + for (const [key, value] of Object.entries(dto)) { + if (typeof value !== 'function' && key in this) { + (this as Record)[key] = value; + } + } + } } export type SettingsDocument = DocumentType; diff --git a/server/src/module/settings/model/ApplicationConfiguration.ts b/server/src/module/settings/model/ApplicationConfiguration.ts index bb73d1419..d1e0f431a 100644 --- a/server/src/module/settings/model/ApplicationConfiguration.ts +++ b/server/src/module/settings/model/ApplicationConfiguration.ts @@ -1,19 +1,9 @@ import { Type } from 'class-transformer'; -import { IsBoolean, IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator'; +import { IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator'; +import { ClientSettingsDTO } from '../settings.dto'; import { DatabaseConfiguration } from './DatabaseConfiguration'; import { MailingConfiguration } from './MailingConfiguration'; -class DefaultSettings implements Partial { - @IsNumber() - @Min(1) - @IsOptional() - defaultTeamSize?: number; - - @IsBoolean() - @IsOptional() - canTutorExcuseStudents?: boolean; -} - export class ApplicationConfiguration { @IsOptional() @IsNumber() @@ -33,7 +23,7 @@ export class ApplicationConfiguration { readonly mailing!: MailingConfiguration; @IsOptional() - @Type(() => DefaultSettings) + @Type(() => ClientSettingsDTO) @ValidateNested() - readonly defaultSettings: DefaultSettings | undefined; + readonly defaultSettings: ClientSettingsDTO | undefined; } diff --git a/server/src/module/settings/settings.controller.ts b/server/src/module/settings/settings.controller.ts index 3477eaa77..dbbfca28c 100644 --- a/server/src/module/settings/settings.controller.ts +++ b/server/src/module/settings/settings.controller.ts @@ -1,14 +1,24 @@ -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Put, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common'; import { AuthenticatedGuard } from '../../guards/authenticated.guard'; +import { HasRoleGuard } from '../../guards/has-role.guard'; +import { IClientSettings } from '../../shared/model/Settings'; +import { ClientSettingsDTO } from './settings.dto'; import { SettingsService } from './settings.service'; -@Controller('settings') +@Controller('setting') export class SettingsController { constructor(private readonly settingsService: SettingsService) {} @Get('/') @UseGuards(AuthenticatedGuard) - async getAllSettings(): Promise { - return this.settingsService.getAllSettings(); + async getAllSettings(): Promise { + return this.settingsService.getClientSettings(); + } + + @Put('/') + @UseGuards(HasRoleGuard) + @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) + async setSettings(@Body() settings: ClientSettingsDTO): Promise { + await this.settingsService.setClientSettings(settings); } } diff --git a/server/src/module/settings/settings.dto.ts b/server/src/module/settings/settings.dto.ts new file mode 100644 index 000000000..2de085fc5 --- /dev/null +++ b/server/src/module/settings/settings.dto.ts @@ -0,0 +1,13 @@ +import { IsBoolean, IsNumber, IsOptional, Min } from 'class-validator'; +import { IClientSettings } from '../../shared/model/Settings'; + +export class ClientSettingsDTO implements Partial { + @IsNumber() + @Min(1) + @IsOptional() + defaultTeamSize?: number; + + @IsBoolean() + @IsOptional() + canTutorExcuseStudents?: boolean; +} diff --git a/server/src/module/settings/settings.service.spec.ts b/server/src/module/settings/settings.service.spec.ts index a21559510..fd8514d88 100644 --- a/server/src/module/settings/settings.service.spec.ts +++ b/server/src/module/settings/settings.service.spec.ts @@ -1,7 +1,23 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { sanitizeObject } from '../../../test/helpers/test.helpers'; import { TestModule } from '../../../test/helpers/test.module'; +import { MockedModel } from '../../../test/helpers/testdocument'; +import { SETTINGS_DOCUMENTS } from '../../../test/mocks/documents.mock'; +import { SettingsModel } from '../../database/models/settings.model'; +import { IClientSettings } from '../../shared/model/Settings'; +import { ClientSettingsDTO } from './settings.dto'; import { SettingsService } from './settings.service'; +interface AssertSettingsParams { + expected: MockedModel; + actual: IClientSettings; +} + +function assertSettings({ expected, actual }: AssertSettingsParams) { + const { _id, ...expectedWithoutId } = sanitizeObject(expected); + expect(actual).toEqual(expectedWithoutId); +} + describe('SettingsService', () => { let testModule: TestingModule; let service: SettingsService; @@ -26,4 +42,20 @@ describe('SettingsService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + it('get all settings', async () => { + const settings = await service.getClientSettings(); + assertSettings({ actual: settings, expected: SETTINGS_DOCUMENTS[0] }); + }); + + it.each([ + { defaultTeamSize: 5 }, + { canTutorExcuseStudents: true }, + { defaultTeamSize: 3, canTutorExcuseStudents: true }, + ])('change setting with DTO "%s"', async (newSetting: ClientSettingsDTO) => { + await service.setClientSettings(newSetting); + + const settings = await service.getClientSettings(); + assertSettings({ actual: settings, expected: { ...SETTINGS_DOCUMENTS[0], ...newSetting } }); + }); }); diff --git a/server/src/module/settings/settings.service.ts b/server/src/module/settings/settings.service.ts index c6bcc4627..4608d52a2 100644 --- a/server/src/module/settings/settings.service.ts +++ b/server/src/module/settings/settings.service.ts @@ -3,6 +3,8 @@ import { ReturnModelType } from '@typegoose/typegoose'; import { InjectModel } from 'nestjs-typegoose'; import { SettingsDocument, SettingsModel } from '../../database/models/settings.model'; import { StartUpException } from '../../exceptions/StartUpException'; +import { IClientSettings } from '../../shared/model/Settings'; +import { ClientSettingsDTO } from './settings.dto'; import { StaticSettings } from './settings.static'; @Injectable() @@ -18,15 +20,29 @@ export class SettingsService extends StaticSettings implements OnModuleInit { } /** - * @returns The current settings. + * @returns The current settings saved in the DB. * * @see getSettingsDocument */ - async getAllSettings(): Promise { + async getClientSettings(): Promise { const document = await this.getSettingsDocument(); return document.toDTO(); } + /** + * Changes the settings saved in the DB to use the new settings. + * + * If `settings` does not contain a property this setting-property will be untouched (ie the previous value will be used). + * + * @param settings New settings to use. + */ + async setClientSettings(settings: ClientSettingsDTO): Promise { + const document = await this.getSettingsDocument(); + + document.assignDTO(settings); + await document.save(); + } + /** * Checks if there is a `SettingsDocument` in the database. * @@ -35,9 +51,9 @@ export class SettingsService extends StaticSettings implements OnModuleInit { * If there is ONE nothing is done. */ async onModuleInit(): Promise { - const document = await this.settingsModel.find(); + const documents = await this.settingsModel.find(); - if (document.length === 0) { + if (documents.length === 0) { this.logger.log('No settings document provided. Creating new default settings document...'); try { diff --git a/server/src/shared/model/Settings.ts b/server/src/shared/model/Settings.ts index ef8abbad3..8a90f0ca7 100644 --- a/server/src/shared/model/Settings.ts +++ b/server/src/shared/model/Settings.ts @@ -1,4 +1,4 @@ -interface ISettings { +export interface IClientSettings { defaultTeamSize: number; canTutorExcuseStudents: boolean; diff --git a/server/test/helpers/test.module.ts b/server/test/helpers/test.module.ts index 59f73b90a..da53bfbb1 100644 --- a/server/test/helpers/test.module.ts +++ b/server/test/helpers/test.module.ts @@ -16,6 +16,7 @@ import { UserModel } from '../../src/database/models/user.model'; import { SCHEINCRITERIA_DOCUMENTS, SCHEINEXAM_DOCUMENTS, + SETTINGS_DOCUMENTS, SHEET_DOCUMENTS, STUDENT_DOCUMENTS, TEAM_DOCUMENTS, @@ -37,7 +38,7 @@ const MODEL_OPTIONS: ModelMockOptions[] = [ { model: ScheinexamModel, initialDocuments: [...SCHEINEXAM_DOCUMENTS] }, { model: ScheincriteriaModel, initialDocuments: [...SCHEINCRITERIA_DOCUMENTS] }, { model: GradingModel, initialDocuments: [] }, - { model: SettingsModel, initialDocuments: [] }, + { model: SettingsModel, initialDocuments: [...SETTINGS_DOCUMENTS] }, ]; @Module({}) diff --git a/server/test/mocks/documents.mock.ts b/server/test/mocks/documents.mock.ts index 4c9fc3d83..ab055927c 100644 --- a/server/test/mocks/documents.mock.ts +++ b/server/test/mocks/documents.mock.ts @@ -2,6 +2,7 @@ import { DateTime } from 'luxon'; import { ExerciseModel, SubExerciseModel } from '../../src/database/models/exercise.model'; import { ScheincriteriaModel } from '../../src/database/models/scheincriteria.model'; import { ScheinexamModel } from '../../src/database/models/scheinexam.model'; +import { SettingsModel } from '../../src/database/models/settings.model'; import { SheetModel } from '../../src/database/models/sheet.model'; import { StudentModel } from '../../src/database/models/student.model'; import { TeamModel } from '../../src/database/models/team.model'; @@ -357,6 +358,10 @@ export const SCHEINCRITERIA_DOCUMENTS: MockedScheincriteriaModel[] = [ }, ]; +export const SETTINGS_DOCUMENTS: MockedModel[] = [ + { _id: '5e59295a14255d6110d892a8', ...new SettingsModel() }, +]; + function generateFakeDocument(_id: string, additional?: Record): any { return { _id, id: _id, ...additional }; } From 5eb99f5f10b9f6e0e1771a8f0f5874df8426b3df Mon Sep 17 00:00:00 2001 From: Dudrie Date: Tue, 28 Jul 2020 18:26:24 +0200 Subject: [PATCH 15/62] Add fetching of settings to the client. This also adds the usage of the defaultTeamSize. --- client/src/components/ContextWrapper.tsx | 25 +- client/src/components/forms/StudentForm.tsx | 219 ++++++++++-------- client/src/hooks/fetching/Settings.ts | 12 + client/src/hooks/useSettings.tsx | 37 +++ .../Studentoverview.helpers.ts | 5 +- .../student-overview/Studentoverview.tsx | 7 +- server/src/module/user/user.service.spec.ts | 42 +++- server/src/shared/model/Settings.ts | 1 - 8 files changed, 232 insertions(+), 116 deletions(-) create mode 100644 client/src/hooks/fetching/Settings.ts create mode 100644 client/src/hooks/useSettings.tsx diff --git a/client/src/components/ContextWrapper.tsx b/client/src/components/ContextWrapper.tsx index 2620a2ab3..bf0087153 100644 --- a/client/src/components/ContextWrapper.tsx +++ b/client/src/components/ContextWrapper.tsx @@ -9,6 +9,7 @@ import { MemoryRouterProps } from 'react-router'; import { BrowserRouterProps } from 'react-router-dom'; import DialogService, { getDialogOutsideContext } from '../hooks/DialogService'; import { LoginContextProvider } from '../hooks/LoginService'; +import { SettingsProvider } from '../hooks/useSettings'; import { RequireChildrenProp } from '../typings/RequireChildrenProp'; import i18n from '../util/lang/configI18N'; import { getRouteWithPrefix } from '../util/routePrefix'; @@ -79,17 +80,19 @@ function handleUserConfirmation(message: string, callback: (ok: boolean) => void function ContextWrapper({ children, Router }: PropsWithChildren): JSX.Element { return ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ); } diff --git a/client/src/components/forms/StudentForm.tsx b/client/src/components/forms/StudentForm.tsx index 3f3d814ef..7423e1f89 100644 --- a/client/src/components/forms/StudentForm.tsx +++ b/client/src/components/forms/StudentForm.tsx @@ -1,13 +1,15 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import { FormikHelpers } from 'formik'; import { AlertOutline as AlertIcon } from 'mdi-material-ui'; -import React, { useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { StudentStatus } from 'shared/model/Student'; import { getNameOfEntity } from 'shared/util/helpers'; import * as Yup from 'yup'; +import { useSettings } from '../../hooks/useSettings'; import { Student } from '../../model/Student'; import { Team } from '../../model/Team'; import { FormikSubmitCallback } from '../../types'; +import Placeholder from '../Placeholder'; import FormikSelect from './components/FormikSelect'; import FormikTextField from './components/FormikTextField'; import FormikBaseForm, { CommonlyUsedFormProps, FormikBaseFormProps } from './FormikBaseForm'; @@ -65,19 +67,18 @@ interface Props extends Omit, CommonlyUsed teams?: Team[]; } -export const CREATE_NEW_TEAM_VALUE = 'CREATE_NEW_TEAM_ACTION'; -type ItemType = Team | { type: typeof CREATE_NEW_TEAM_VALUE }; - -function getMaxTeamSize() { - // TODO: Replace with settings after settings are implemented. - return 2; +interface InitialStateParams { + teams?: Team[]; + student?: Student; + defaultTeamSize: number; } -function getNextTeamWithSlot(teams: Team[]): string { - const maxTeamSize = getMaxTeamSize(); +export const CREATE_NEW_TEAM_VALUE = 'CREATE_NEW_TEAM_ACTION'; +type ItemType = Team | { type: typeof CREATE_NEW_TEAM_VALUE }; +function getNextTeamWithSlot(teams: Team[], defaultTeamSize: number): string { for (const team of teams) { - if (team.students.length < maxTeamSize) { + if (team.students.length < defaultTeamSize) { return team.id; } } @@ -85,7 +86,11 @@ function getNextTeamWithSlot(teams: Team[]): string { return CREATE_NEW_TEAM_VALUE; } -export function getInitialStudentFormState(teams?: Team[], student?: Student): StudentFormState { +export function getInitialStudentFormState({ + teams, + student, + defaultTeamSize, +}: InitialStateParams): StudentFormState { if (student) { return { lastname: student.lastname, @@ -105,7 +110,7 @@ export function getInitialStudentFormState(teams?: Team[], student?: Student): S email: '', courseOfStudies: '', status: StudentStatus.ACTIVE, - team: teams ? getNextTeamWithSlot(teams) : '', + team: teams ? getNextTeamWithSlot(teams, defaultTeamSize) : '', }; } @@ -151,7 +156,14 @@ function StudentForm({ }: Props): JSX.Element { const classes = useStyles(); const firstnameInputRef = useRef(); - const initialFormState = getInitialStudentFormState(teamsFromProps, student); + + const { + settings: { defaultTeamSize }, + isLoadingSettings, + } = useSettings(); + const initialFormState = useMemo(() => { + return getInitialStudentFormState({ teams: teamsFromProps, student, defaultTeamSize }); + }, [teamsFromProps, student, defaultTeamSize]); const disableTeamDropdown = !teamsFromProps; const teams: ItemType[] = [{ type: CREATE_NEW_TEAM_VALUE }, ...(teamsFromProps || [])]; @@ -171,94 +183,105 @@ function StudentForm({ const availableStatuses = Object.values(StudentStatus); return ( - - {({ values }) => ( - <> - - - - - { - if (!value) { - return undefined; - } + + {({ values }) => ( + <> + + + + + { + if (!value) { + return undefined; + } - for (const s of otherStudents) { - if (s.matriculationNo && value === s.matriculationNo) { - return `Matrikelnummer wird bereits von ${getNameOfEntity(s, { - firstNameFirst: true, - })} verwendet.`; + for (const s of otherStudents) { + if (s.matriculationNo && value === s.matriculationNo) { + return `Matrikelnummer wird bereits von ${getNameOfEntity(s, { + firstNameFirst: true, + })} verwendet.`; + } } - } - - return undefined; - }, - }} - inputProps={{ - min: 0, - }} - helperText={ - values['matriculationNo'] === '' ? 'Keine Matrikelnummer eingegeben.' : undefined - } - InputProps={{ - endAdornment: - values['matriculationNo'] === '' ? ( - - ) : undefined, - classes: { - notchedOutline: - values['matriculationNo'] === '' ? classes.warningBorder : undefined, - }, - }} - InputLabelProps={{ - classes: { - root: values['matriculationNo'] === '' ? classes.warningColor : undefined, - }, - }} - FormHelperTextProps={{ - classes: { - root: values['matriculationNo'] === '' ? classes.warningColor : undefined, - }, - }} - /> - - - - - - - - s} - /> - - )} - + + return undefined; + }, + }} + inputProps={{ + min: 0, + }} + helperText={ + values['matriculationNo'] === '' ? 'Keine Matrikelnummer eingegeben.' : undefined + } + InputProps={{ + endAdornment: + values['matriculationNo'] === '' ? ( + + ) : undefined, + classes: { + notchedOutline: + values['matriculationNo'] === '' ? classes.warningBorder : undefined, + }, + }} + InputLabelProps={{ + classes: { + root: values['matriculationNo'] === '' ? classes.warningColor : undefined, + }, + }} + FormHelperTextProps={{ + classes: { + root: values['matriculationNo'] === '' ? classes.warningColor : undefined, + }, + }} + /> + + + + + + + + s} + /> + + )} + + ); } diff --git a/client/src/hooks/fetching/Settings.ts b/client/src/hooks/fetching/Settings.ts new file mode 100644 index 000000000..ee8909671 --- /dev/null +++ b/client/src/hooks/fetching/Settings.ts @@ -0,0 +1,12 @@ +import { IClientSettings } from 'shared/model/Settings'; +import axios from './Axios'; + +export async function getSettings(): Promise { + const response = await axios.get(`setting`); + + if (response.status === 200) { + return response.data; + } + + return Promise.reject(`Wrong status code (${response.status}).`); +} diff --git a/client/src/hooks/useSettings.tsx b/client/src/hooks/useSettings.tsx new file mode 100644 index 000000000..929428666 --- /dev/null +++ b/client/src/hooks/useSettings.tsx @@ -0,0 +1,37 @@ +import React, { useContext } from 'react'; +import { IClientSettings } from 'shared/model/Settings'; +import { RequireChildrenProp } from '../typings/RequireChildrenProp'; +import { getSettings } from './fetching/Settings'; +import { useFetchState } from './useFetchState'; + +interface ContextType { + settings: IClientSettings; + isLoadingSettings: boolean; +} + +const DEFAULT_SETTINGS: IClientSettings = { defaultTeamSize: 1, canTutorExcuseStudents: false }; + +const SettingsContext = React.createContext({ + settings: DEFAULT_SETTINGS, + isLoadingSettings: false, +}); + +export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element { + const { value, isLoading } = useFetchState({ + fetchFunction: getSettings, + immediate: true, + params: [], + }); + + return ( + + {children} + + ); +} + +export function useSettings(): ContextType { + return useContext(SettingsContext); +} diff --git a/client/src/view/studentmanagement/student-overview/Studentoverview.helpers.ts b/client/src/view/studentmanagement/student-overview/Studentoverview.helpers.ts index 2a8ea59d5..65e9034d0 100644 --- a/client/src/view/studentmanagement/student-overview/Studentoverview.helpers.ts +++ b/client/src/view/studentmanagement/student-overview/Studentoverview.helpers.ts @@ -66,7 +66,8 @@ export function handleCreateStudent({ tutorialId, dispatch, enqueueSnackbar, -}: HandlerParams): StudentFormSubmitCallback { + defaultTeamSize, +}: HandlerParams & { defaultTeamSize: number }): StudentFormSubmitCallback { return async ( { firstname, lastname, matriculationNo, email, courseOfStudies, team, status }, { setSubmitting, resetForm } @@ -92,7 +93,7 @@ export function handleCreateStudent({ }); const teams = await getTeamsOfTutorial(tutorialId); - resetForm({ values: getInitialStudentFormState(teams) }); + resetForm({ values: getInitialStudentFormState({ teams, defaultTeamSize }) }); enqueueSnackbar('Student/in wurde erfolgreich erstellt.', { variant: 'success' }); } catch (reason) { Logger.logger.error(reason, { context: 'Studentoverview' }); diff --git a/client/src/view/studentmanagement/student-overview/Studentoverview.tsx b/client/src/view/studentmanagement/student-overview/Studentoverview.tsx index f919c56c6..c74b543cf 100644 --- a/client/src/view/studentmanagement/student-overview/Studentoverview.tsx +++ b/client/src/view/studentmanagement/student-overview/Studentoverview.tsx @@ -11,6 +11,7 @@ import LoadingSpinner from '../../../components/loading/LoadingSpinner'; import TableWithForm from '../../../components/TableWithForm'; import TableWithPadding from '../../../components/TableWithPadding'; import { useDialog } from '../../../hooks/DialogService'; +import { useSettings } from '../../../hooks/useSettings'; import { Student } from '../../../model/Student'; import { Tutorial } from '../../../model/Tutorial'; import { useStudentStore } from '../student-store/StudentStore'; @@ -65,6 +66,10 @@ function Studentoverview({ const [{ students, teams, tutorialId, isInitialized, summaries }, dispatch] = useStudentStore(); const { enqueueSnackbar } = useSnackbar(); + const { + settings: { defaultTeamSize }, + } = useSettings(); + const handlerParams: HandlerParams = { tutorialId, dispatch, enqueueSnackbar }; if (!isInitialized) { @@ -202,7 +207,7 @@ function Studentoverview({ } items={getFilteredStudents(students, filterText, sortOption)} diff --git a/server/src/module/user/user.service.spec.ts b/server/src/module/user/user.service.spec.ts index 4d445d127..39e4a1627 100644 --- a/server/src/module/user/user.service.spec.ts +++ b/server/src/module/user/user.service.spec.ts @@ -6,12 +6,12 @@ import { TestModule } from '../../../test/helpers/test.module'; import { MockedModel } from '../../../test/helpers/testdocument'; import { TUTORIAL_DOCUMENTS, USER_DOCUMENTS } from '../../../test/mocks/documents.mock'; import { UserModel } from '../../database/models/user.model'; +import { NamedElement } from '../../shared/model/Common'; import { Role } from '../../shared/model/Role'; -import { IUser } from '../../shared/model/User'; +import { ILoggedInUser, IUser } from '../../shared/model/User'; import { TutorialService } from '../tutorial/tutorial.service'; +import { CreateUserDTO, UserDTO } from './user.dto'; import { UserService } from './user.service'; -import { UserDTO, CreateUserDTO } from './user.dto'; -import { NamedElement } from '../../shared/model/Common'; interface AssertUserParam { expected: MockedModel; @@ -33,6 +33,11 @@ interface AssertGeneratedUsersParams { actual: IUser[]; } +interface AssertLoggedInUserParams { + expected: MockedModel; + actual: ILoggedInUser; +} + /** * Checks if the given user representations are considered equal. * @@ -136,6 +141,30 @@ function assertGeneratedUsers({ expected, actual }: AssertGeneratedUsersParams) } } +function assertLoggedInUser({ expected, actual }: AssertLoggedInUserParams) { + const { + _id, + firstname, + lastname, + roles, + temporaryPassword, + tutorials, + tutorialsToCorrect, + } = sanitizeObject(expected); + + expect(actual.id).toBe(_id); + expect(actual.firstname).toBe(firstname); + expect(actual.lastname).toBe(lastname); + expect(actual.roles).toEqual(roles); + expect(actual.hasTemporaryPassword).toBe(!!temporaryPassword); + + expect(actual.tutorials.map((t) => t.id)).toEqual(tutorials.map((t) => t.id)); + expect(actual.tutorialsToCorrect.map((t) => t.id)).toEqual(tutorialsToCorrect.map((t) => t.id)); + + // TODO: Test substituteTutorials! + // expect(actual.substituteTutorials).toEqual +} + describe('UserService', () => { let testModule: TestingModule; let service: UserService; @@ -734,4 +763,11 @@ describe('UserService', () => { const namesOfTutors: NamedElement[] = await service.getNamesOfAllTutors(); expect(namesOfTutors).toEqual(expected); }); + + it('get user information on log in', async () => { + const idOfUser = USER_DOCUMENTS[2]._id; + const userInformation = await service.getLoggedInUserInformation(idOfUser); + + assertLoggedInUser({ expected: USER_DOCUMENTS[0], actual: userInformation }); + }); }); diff --git a/server/src/shared/model/Settings.ts b/server/src/shared/model/Settings.ts index 8a90f0ca7..688f725d5 100644 --- a/server/src/shared/model/Settings.ts +++ b/server/src/shared/model/Settings.ts @@ -1,5 +1,4 @@ export interface IClientSettings { defaultTeamSize: number; - canTutorExcuseStudents: boolean; } From 4dd05e1c5405c9a18d52b15add4baa6151ffe2aa Mon Sep 17 00:00:00 2001 From: Dudrie Date: Tue, 28 Jul 2020 20:10:01 +0200 Subject: [PATCH 16/62] Add a check if the user would be allowed to excuse a student. --- .../src/module/student/student.controller.ts | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/server/src/module/student/student.controller.ts b/server/src/module/student/student.controller.ts index b9f597557..19b626d32 100644 --- a/server/src/module/student/student.controller.ts +++ b/server/src/module/student/student.controller.ts @@ -1,7 +1,9 @@ import { + BadRequestException, Body, Controller, Delete, + ForbiddenException, Get, HttpCode, HttpStatus, @@ -9,19 +11,23 @@ import { Patch, Post, Put, + Request, UseGuards, UsePipes, ValidationPipe, } from '@nestjs/common'; +import { Request as ExpressRequest } from 'express'; +import { DateTime } from 'luxon'; import { CreatedInOwnTutorialGuard } from '../../guards/created-in-own-tutorial.guard'; import { AllowCorrectors } from '../../guards/decorators/allowCorrectors.decorator'; import { AllowSubstitutes } from '../../guards/decorators/allowSubstitutes.decorator'; import { Roles } from '../../guards/decorators/roles.decorator'; import { HasRoleGuard } from '../../guards/has-role.guard'; import { StudentGuard } from '../../guards/student.guard'; -import { IAttendance } from '../../shared/model/Attendance'; +import { AttendanceState, IAttendance } from '../../shared/model/Attendance'; import { Role } from '../../shared/model/Role'; import { IStudent } from '../../shared/model/Student'; +import { SettingsService } from '../settings/settings.service'; import { AttendanceDTO, CakeCountDTO, @@ -31,9 +37,18 @@ import { } from './student.dto'; import { StudentService } from './student.service'; +interface CheckCanExcuseParams { + dto: AttendanceDTO; + studentId: string; + user?: Express.User; +} + @Controller('student') export class StudentController { - constructor(private readonly studentService: StudentService) {} + constructor( + private readonly studentService: StudentService, + private readonly settingsService: SettingsService + ) {} @Get() @UseGuards(HasRoleGuard) @@ -86,8 +101,11 @@ export class StudentController { @UsePipes(ValidationPipe) async updateAttendance( @Param('id') id: string, - @Body() dto: AttendanceDTO + @Body() dto: AttendanceDTO, + @Request() request: ExpressRequest ): Promise { + await this.checkUserCanExcuseOrThrow({ dto, studentId: id, user: request.user }); + const attendance = await this.studentService.setAttendance(id, dto); return attendance; @@ -122,4 +140,46 @@ export class StudentController { async updateCakeCount(@Param('id') id: string, @Body() dto: CakeCountDTO): Promise { await this.studentService.setCakeCount(id, dto); } + + /** + * Checks if the given user is allowed to proceed with the request to change the attendance state in regards to the application settings. + * + * __Important__: This does __NOT__ check if the user is allowed in general (ie correct role, is tutor, ...). Those checks must still be performed by the corresponding route guard! + * + * This checks if all of the following conditions are met. If so an exception is thrown: + * - The application settings disallow non-admins to excuse a student. + * - The user making the request is __not__ an admin. + * - The DTO would change the attendance state of a student to `excused`. + * + * @param params Must contain the `studentId`, the `dto` of the request and the `user` making the request (optional). + * + * @throws `BadRequestException` - If the given `user` is not defined. + * @throws `ForbiddenException` - If the `user` is not allowed to proceed with the request (see above). + */ + private async checkUserCanExcuseOrThrow({ + user, + dto, + studentId, + }: CheckCanExcuseParams): Promise { + if (!user) { + throw new BadRequestException('No user available in request.'); + } + + const settings = await this.settingsService.getClientSettings(); + const student = await this.studentService.findById(studentId); + const wouldChangeAttendance = + student.getAttendance(DateTime.fromISO(dto.date))?.state !== dto.state; + + if (!wouldChangeAttendance) { + return; + } + + if ( + !settings.canTutorExcuseStudents && + !user.roles.includes(Role.ADMIN) && + dto.state === AttendanceState.EXCUSED + ) { + throw new ForbiddenException('User is not allowed to excuse a student.'); + } + } } From dbf83ff68e3772cdb858514c48e0d262620571cd Mon Sep 17 00:00:00 2001 From: Dudrie Date: Tue, 28 Jul 2020 20:11:33 +0200 Subject: [PATCH 17/62] Add usage of canTutorExcuseStudent setting in client. --- .../AttendanceControls.tsx | 22 +++++-------------- .../src/view/attendance/AttendanceManager.tsx | 22 ++++++++++++++++--- .../components/StudentsAttendanceRow.tsx | 7 ++++-- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/client/src/components/attendance-controls/AttendanceControls.tsx b/client/src/components/attendance-controls/AttendanceControls.tsx index 042dc74d1..0735e1c1a 100644 --- a/client/src/components/attendance-controls/AttendanceControls.tsx +++ b/client/src/components/attendance-controls/AttendanceControls.tsx @@ -1,8 +1,6 @@ import { Box, BoxProps } from '@material-ui/core'; import React from 'react'; -import { IAttendance, AttendanceState } from 'shared/model/Attendance'; -import { Role } from 'shared/model/Role'; -import { useLogin } from '../../hooks/LoginService'; +import { AttendanceState, IAttendance } from 'shared/model/Attendance'; import AttendanceButton from '../../view/attendance/components/AttendanceButton'; import AttendanceNotePopper, { NoteFormCallback } from './components/AttendanceNotePopper'; @@ -10,16 +8,16 @@ export interface AttendanceControlsProps extends BoxProps { attendance?: IAttendance; onAttendanceChange: (attendance?: AttendanceState) => void; onNoteChange: NoteFormCallback; + excuseDisabled: boolean; } function AttendanceControls({ attendance, onAttendanceChange, onNoteChange, + excuseDisabled, ...props }: AttendanceControlsProps): JSX.Element { - const { userData } = useLogin(); - const handleAttendanceClick = (newState: AttendanceState) => { if (newState === attendance?.state) { onAttendanceChange(undefined); @@ -36,11 +34,7 @@ function AttendanceControls({ attendanceState={AttendanceState.PRESENT} isSelected={attendance?.state === AttendanceState.PRESENT} onClick={() => handleAttendanceClick(AttendanceState.PRESENT)} - disabled={ - userData && - !userData.roles.includes(Role.ADMIN) && - attendance?.state === AttendanceState.EXCUSED - } + disabled={excuseDisabled && attendance?.state === AttendanceState.EXCUSED} > Anwesend @@ -49,7 +43,7 @@ function AttendanceControls({ attendanceState={AttendanceState.EXCUSED} isSelected={attendance?.state === AttendanceState.EXCUSED} onClick={() => handleAttendanceClick(AttendanceState.EXCUSED)} - disabled={userData && userData.roles.includes(Role.ADMIN) ? false : true} + disabled={excuseDisabled} > Entschuldigt @@ -58,11 +52,7 @@ function AttendanceControls({ attendanceState={AttendanceState.UNEXCUSED} isSelected={attendance?.state === AttendanceState.UNEXCUSED} onClick={() => handleAttendanceClick(AttendanceState.UNEXCUSED)} - disabled={ - userData && - !userData.roles.includes(Role.ADMIN) && - attendance?.state === AttendanceState.EXCUSED - } + disabled={excuseDisabled && attendance?.state === AttendanceState.EXCUSED} > Unentschuldigt diff --git a/client/src/view/attendance/AttendanceManager.tsx b/client/src/view/attendance/AttendanceManager.tsx index 57e6ebd7a..c452aa4cc 100644 --- a/client/src/view/attendance/AttendanceManager.tsx +++ b/client/src/view/attendance/AttendanceManager.tsx @@ -3,8 +3,9 @@ import GREEN from '@material-ui/core/colors/green'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import { DateTime } from 'luxon'; import { useSnackbar } from 'notistack'; -import React, { ChangeEvent, useEffect, useState } from 'react'; +import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'; import { AttendanceState, IAttendance, IAttendanceDTO } from 'shared/model/Attendance'; +import { Role } from 'shared/model/Role'; import { StudentStatus } from 'shared/model/Student'; import { NoteFormCallback } from '../../components/attendance-controls/components/AttendanceNotePopper'; import CustomSelect from '../../components/CustomSelect'; @@ -20,6 +21,7 @@ import { } from '../../hooks/fetching/Student'; import { getAllTutorials, getStudentsOfTutorial, getTutorial } from '../../hooks/fetching/Tutorial'; import { useLogin } from '../../hooks/LoginService'; +import { useSettings } from '../../hooks/useSettings'; import { LoggedInUser } from '../../model/LoggedInUser'; import { Student } from '../../model/Student'; import { Tutorial } from '../../model/Tutorial'; @@ -110,9 +112,11 @@ function getFilteredStudents(allStudents: Student[], filterOption: FilterOption) function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element { const classes = useStyles(); + const logger = useLogger('AttendanceManager'); + const { userData } = useLogin(); + const { settings } = useSettings(); const { enqueueSnackbar } = useSnackbar(); - const logger = useLogger('AttendanceManager'); const [isLoading, setIsLoading] = useState(false); const [isLoadingPDF, setLoadingPDF] = useState(false); @@ -128,7 +132,18 @@ function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element const [filterOption, setFilterOption] = useState(FilterOption.ACTIVE_ONLY); - const availableDates = getAvailableDates(tutorial, userData, !tutorialFromProps); + const availableDates = useMemo(() => getAvailableDates(tutorial, userData, !tutorialFromProps), [ + tutorial, + userData, + tutorialFromProps, + ]); + const canStudentBeExcused = useMemo(() => { + if (settings.canTutorExcuseStudents) { + return true; + } + + return !!userData && userData.roles.includes(Role.ADMIN); + }, [userData, settings.canTutorExcuseStudents]); useEffect(() => { if (!!tutorialFromProps) { @@ -391,6 +406,7 @@ function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element onAttendanceSelection={(state) => handleStudentAttendanceChange(student, state)} onNoteSave={handleStudentNoteChange(student)} onCakeCountChanged={handleCakeCountChange(student)} + canBeExcused={canStudentBeExcused} /> ); }} diff --git a/client/src/view/attendance/components/StudentsAttendanceRow.tsx b/client/src/view/attendance/components/StudentsAttendanceRow.tsx index 426a0a383..af31d398d 100644 --- a/client/src/view/attendance/components/StudentsAttendanceRow.tsx +++ b/client/src/view/attendance/components/StudentsAttendanceRow.tsx @@ -2,14 +2,14 @@ import { TableCell } from '@material-ui/core'; import { createStyles, makeStyles, Theme, useTheme } from '@material-ui/core/styles'; import clsx from 'clsx'; import React from 'react'; -import { IAttendance, AttendanceState } from 'shared/model/Attendance'; +import { AttendanceState, IAttendance } from 'shared/model/Attendance'; import AttendanceControls from '../../../components/attendance-controls/AttendanceControls'; import { NoteFormCallback } from '../../../components/attendance-controls/components/AttendanceNotePopper'; import PaperTableRow from '../../../components/PaperTableRow'; import StudentAvatar from '../../../components/student-icon/StudentAvatar'; +import { Student } from '../../../model/Student'; import { getAttendanceColor } from './AttendanceButton'; import CakeCount from './CakeCount'; -import { Student } from '../../../model/Student'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -28,6 +28,7 @@ interface Props { onAttendanceSelection: (state?: AttendanceState) => void; onNoteSave: NoteFormCallback; onCakeCountChanged?: (cakeCount: number) => void; + canBeExcused: boolean; } function StudentAttendanceRow({ @@ -36,6 +37,7 @@ function StudentAttendanceRow({ onAttendanceSelection, onNoteSave, onCakeCountChanged, + canBeExcused, ...rest }: Props): JSX.Element { const classes = useStyles(); @@ -67,6 +69,7 @@ function StudentAttendanceRow({ onAttendanceChange={onAttendanceSelection} onNoteChange={onNoteSave} justifyContent='flex-end' + excuseDisabled={!canBeExcused} /> From 86a36f39d7f0ab3e7ebc8c65a457caa439d75637 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Tue, 28 Jul 2020 20:14:45 +0200 Subject: [PATCH 18/62] Fix failing UserService test. --- server/src/module/user/user.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/module/user/user.service.spec.ts b/server/src/module/user/user.service.spec.ts index 39e4a1627..5d7992495 100644 --- a/server/src/module/user/user.service.spec.ts +++ b/server/src/module/user/user.service.spec.ts @@ -765,9 +765,9 @@ describe('UserService', () => { }); it('get user information on log in', async () => { - const idOfUser = USER_DOCUMENTS[2]._id; - const userInformation = await service.getLoggedInUserInformation(idOfUser); + const expected = USER_DOCUMENTS[2]; + const userInformation = await service.getLoggedInUserInformation(expected._id); - assertLoggedInUser({ expected: USER_DOCUMENTS[0], actual: userInformation }); + assertLoggedInUser({ expected, actual: userInformation }); }); }); From de6851101fc38fa728e914108c42ac013049b18f Mon Sep 17 00:00:00 2001 From: Dudrie Date: Tue, 28 Jul 2020 20:24:08 +0200 Subject: [PATCH 19/62] Fix UserModel.toDTO returning a CoreMongooseArray for roles. The roles in the DTO should be a standard JS array. However, besides failing tests, this did not have any implications for the app because the type information was lost by strinigfying the response bodies to JSON anyway. --- server/src/database/models/user.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/database/models/user.model.ts b/server/src/database/models/user.model.ts index 3eada8067..c9e793f57 100644 --- a/server/src/database/models/user.model.ts +++ b/server/src/database/models/user.model.ts @@ -113,7 +113,7 @@ export class UserModel { username, firstname, lastname, - roles, + roles: [...roles], email, temporaryPassword, tutorials: tutorials.map((tutorial) => ({ From a06d19678fa4488f9045efc6015507b1522d69be Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 11:58:40 +0200 Subject: [PATCH 20/62] Rename folder view/ -> pages/. --- client/src/components/ContextWrapper.tsx | 16 +++---- .../AttendanceControls.tsx | 2 +- .../context/StepperContext.tsx | 16 +++---- client/src/index.tsx | 4 +- client/src/{view => pages}/App.tsx | 0 client/src/{view => pages}/AppBar.tsx | 0 client/src/{view => pages}/Login.tsx | 0 .../attendance/AttendanceAdminView.tsx | 0 .../attendance/AttendanceManager.tsx | 12 +---- .../attendance/AttendanceView.tsx | 0 .../components/AttendanceButton.tsx | 0 .../attendance/components/CakeCount.tsx | 0 .../components/StudentsAttendanceRow.tsx | 0 .../criteria-info-view/CriteriaInfoView.tsx | 0 .../{view => pages}/dashboard/Dashboard.tsx | 0 .../dashboard/components/AdminStatsCard.tsx | 0 .../components/AllTutorialStatistics.tsx | 0 .../dashboard/components/AspectRatio.tsx | 0 .../components/ScheinCrtieriaStatsCard.tsx | 0 .../components/ScheinPassedStatsCard.tsx | 0 .../components/TutorialStatistics.tsx | 0 .../components/TutorialStatsCard.tsx | 0 .../generate-tutorials/GenerateTutorials.tsx | 0 .../GenerateTutorials.validation.tsx | 0 .../excluded-dates/ExcludedDateBox.tsx | 0 .../excluded-dates/ExcludedDateDialog.tsx | 0 .../excluded-dates/FormikExcludedDates.tsx | 0 .../components/weekday-slots/AddSlotForm.tsx | 0 .../weekday-slots/FormikWeekdaySlot.tsx | 0 .../components/weekday-slots/IconForTab.tsx | 0 .../components/weekday-slots/WeekdayBox.tsx | 0 .../components/weekday-slots/WeekdayTabs.tsx | 0 .../import-data/ImportUsers.context.tsx | 8 ++-- .../import-data/ImportUsers.tsx | 0 .../AdjustImportedUserDataForm.tsx | 0 .../components/UserDataBox.helpers.ts | 0 .../components/UserDataBox.tsx | 0 .../components/UserDataRow.tsx | 0 .../edit-user-dialog/EditUserDialog.tsx | 0 .../EditUserDialogContent.tsx | 0 .../import-data/import-csv/ImportUserCSV.tsx | 0 .../import-data/map-columns/MapCSVColumns.tsx | 0 .../management/components/InfoTable.tsx | 0 .../components/ScheinCriteriaStatusTable.tsx | 0 .../management/components/StatusProgress.tsx | 0 .../enter-form/EnterScheinexamPoints.tsx | 0 .../components/ScheinexamPointsForm.tsx | 0 .../overview/ScheinexamPointsOverview.tsx | 0 .../overview/components/StudentCard.tsx | 0 .../overview/components/StudentCardList.tsx | 0 .../enter-form/EnterPoints.helpers.ts | 0 .../points-sheet/enter-form/EnterPoints.tsx | 0 .../enter-form/EnterStudentPoints.tsx | 0 .../enter-form/EnterTeamPoints.tsx | 0 .../components/EnterPointsForm.helpers.ts | 0 .../enter-form/components/EnterPointsForm.tsx | 0 .../enter-form/components/ExerciseBox.tsx | 0 .../points-sheet/overview/PointsOverview.tsx | 0 .../overview/components/TeamCard.tsx | 0 .../overview/components/TeamCardList.tsx | 0 .../points-sheet/util/helper.ts | 0 .../PresentationPoints.tsx | 0 .../components/PresentationList.tsx | 0 .../ScheinCriteriaManagement.tsx | 0 .../components/ScheinCriteriaRow.tsx | 0 .../ScheinExamManagement.tsx | 0 .../components/ScheinExamRow.tsx | 0 .../sheetmanagement/SheetManagement.tsx | 0 .../sheetmanagement/components/SheetRow.tsx | 0 .../student-info/StudentInfo.tsx | 28 +++++------ .../components/AttendanceInformation.tsx | 9 ++-- .../components/CriteriaCharts.tsx | 2 +- .../components/EvaluationInformation.tsx | 18 +++---- .../components/ScheinExamInformation.tsx | 12 ++--- .../components/ScheinStatusBox.tsx | 0 .../components/StudentDetails.tsx | 4 +- .../AllStudentsAdminView.tsx | 0 .../TutorStudentmanagement.tsx | 4 +- .../Studentoverview.helpers.ts | 0 .../student-overview/Studentoverview.tsx | 0 .../components/StudenRow.helpers.ts | 0 .../components/StudentRow.tsx | 2 +- .../student-store/StudentStore.actions.ts | 0 .../student-store/StudentStore.reducers.ts | 0 .../student-store/StudentStore.tsx | 0 .../teamoverview/Teamoverview.tsx | 0 .../components/CreateTeamDialog.tsx | 0 .../TutorialInternalsManagement.tsx | 0 .../components/Routes.tsx | 0 .../SubstituteManagement.context.tsx | 16 +++---- .../SubstituteManagement.tsx | 0 .../SubstituteManagement.types.ts | 0 .../components/DateBox.tsx | 0 .../components/DateButton.tsx | 0 .../components/ListOfTutors.tsx | 0 .../components/SelectSubstitute.tsx | 0 .../components/SelectedSubstituteBar.tsx | 0 .../tutorialmanagement/TutorialManagement.tsx | 0 .../components/TutorialTableRow.tsx | 0 .../usermanagement/UserManagement.tsx | 0 .../components/UserTableRow.tsx | 0 client/src/routes/Routing.routes.ts | 48 +++++++++---------- client/src/test/App.test.tsx | 2 +- client/src/util/throwFunctions.ts | 2 +- 104 files changed, 98 insertions(+), 107 deletions(-) rename client/src/{view => pages}/App.tsx (100%) rename client/src/{view => pages}/AppBar.tsx (100%) rename client/src/{view => pages}/Login.tsx (100%) rename client/src/{view => pages}/attendance/AttendanceAdminView.tsx (100%) rename client/src/{view => pages}/attendance/AttendanceManager.tsx (97%) rename client/src/{view => pages}/attendance/AttendanceView.tsx (100%) rename client/src/{view => pages}/attendance/components/AttendanceButton.tsx (100%) rename client/src/{view => pages}/attendance/components/CakeCount.tsx (100%) rename client/src/{view => pages}/attendance/components/StudentsAttendanceRow.tsx (100%) rename client/src/{view => pages}/criteria-info-view/CriteriaInfoView.tsx (100%) rename client/src/{view => pages}/dashboard/Dashboard.tsx (100%) rename client/src/{view => pages}/dashboard/components/AdminStatsCard.tsx (100%) rename client/src/{view => pages}/dashboard/components/AllTutorialStatistics.tsx (100%) rename client/src/{view => pages}/dashboard/components/AspectRatio.tsx (100%) rename client/src/{view => pages}/dashboard/components/ScheinCrtieriaStatsCard.tsx (100%) rename client/src/{view => pages}/dashboard/components/ScheinPassedStatsCard.tsx (100%) rename client/src/{view => pages}/dashboard/components/TutorialStatistics.tsx (100%) rename client/src/{view => pages}/dashboard/components/TutorialStatsCard.tsx (100%) rename client/src/{view => pages}/generate-tutorials/GenerateTutorials.tsx (100%) rename client/src/{view => pages}/generate-tutorials/GenerateTutorials.validation.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/excluded-dates/ExcludedDateBox.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/excluded-dates/ExcludedDateDialog.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/weekday-slots/AddSlotForm.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/weekday-slots/FormikWeekdaySlot.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/weekday-slots/IconForTab.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/weekday-slots/WeekdayBox.tsx (100%) rename client/src/{view => pages}/generate-tutorials/components/weekday-slots/WeekdayTabs.tsx (100%) rename client/src/{view => pages}/import-data/ImportUsers.context.tsx (91%) rename client/src/{view => pages}/import-data/ImportUsers.tsx (100%) rename client/src/{view => pages}/import-data/adjust-data-form/AdjustImportedUserDataForm.tsx (100%) rename client/src/{view => pages}/import-data/adjust-data-form/components/UserDataBox.helpers.ts (100%) rename client/src/{view => pages}/import-data/adjust-data-form/components/UserDataBox.tsx (100%) rename client/src/{view => pages}/import-data/adjust-data-form/components/UserDataRow.tsx (100%) rename client/src/{view => pages}/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialog.tsx (100%) rename client/src/{view => pages}/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialogContent.tsx (100%) rename client/src/{view => pages}/import-data/import-csv/ImportUserCSV.tsx (100%) rename client/src/{view => pages}/import-data/map-columns/MapCSVColumns.tsx (100%) rename client/src/{view => pages}/management/components/InfoTable.tsx (100%) rename client/src/{view => pages}/management/components/ScheinCriteriaStatusTable.tsx (100%) rename client/src/{view => pages}/management/components/StatusProgress.tsx (100%) rename client/src/{view => pages}/points-scheinexam/enter-form/EnterScheinexamPoints.tsx (100%) rename client/src/{view => pages}/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx (100%) rename client/src/{view => pages}/points-scheinexam/overview/ScheinexamPointsOverview.tsx (100%) rename client/src/{view => pages}/points-scheinexam/overview/components/StudentCard.tsx (100%) rename client/src/{view => pages}/points-scheinexam/overview/components/StudentCardList.tsx (100%) rename client/src/{view => pages}/points-sheet/enter-form/EnterPoints.helpers.ts (100%) rename client/src/{view => pages}/points-sheet/enter-form/EnterPoints.tsx (100%) rename client/src/{view => pages}/points-sheet/enter-form/EnterStudentPoints.tsx (100%) rename client/src/{view => pages}/points-sheet/enter-form/EnterTeamPoints.tsx (100%) rename client/src/{view => pages}/points-sheet/enter-form/components/EnterPointsForm.helpers.ts (100%) rename client/src/{view => pages}/points-sheet/enter-form/components/EnterPointsForm.tsx (100%) rename client/src/{view => pages}/points-sheet/enter-form/components/ExerciseBox.tsx (100%) rename client/src/{view => pages}/points-sheet/overview/PointsOverview.tsx (100%) rename client/src/{view => pages}/points-sheet/overview/components/TeamCard.tsx (100%) rename client/src/{view => pages}/points-sheet/overview/components/TeamCardList.tsx (100%) rename client/src/{view => pages}/points-sheet/util/helper.ts (100%) rename client/src/{view => pages}/presentation-points/PresentationPoints.tsx (100%) rename client/src/{view => pages}/presentation-points/components/PresentationList.tsx (100%) rename client/src/{view => pages}/scheincriteriamanagement/ScheinCriteriaManagement.tsx (100%) rename client/src/{view => pages}/scheincriteriamanagement/components/ScheinCriteriaRow.tsx (100%) rename client/src/{view => pages}/scheinexam-management/ScheinExamManagement.tsx (100%) rename client/src/{view => pages}/scheinexam-management/components/ScheinExamRow.tsx (100%) rename client/src/{view => pages}/sheetmanagement/SheetManagement.tsx (100%) rename client/src/{view => pages}/sheetmanagement/components/SheetRow.tsx (100%) rename client/src/{view/studentmanagement => pages}/student-info/StudentInfo.tsx (88%) rename client/src/{view/studentmanagement => pages}/student-info/components/AttendanceInformation.tsx (85%) rename client/src/{view/studentmanagement => pages}/student-info/components/CriteriaCharts.tsx (95%) rename client/src/{view/studentmanagement => pages}/student-info/components/EvaluationInformation.tsx (84%) rename client/src/{view/studentmanagement => pages}/student-info/components/ScheinExamInformation.tsx (85%) rename client/src/{view/studentmanagement => pages}/student-info/components/ScheinStatusBox.tsx (100%) rename client/src/{view/studentmanagement => pages}/student-info/components/StudentDetails.tsx (89%) rename client/src/{view => pages}/studentmanagement/AllStudentsAdminView.tsx (100%) rename client/src/{view => pages}/studentmanagement/TutorStudentmanagement.tsx (93%) rename client/src/{view => pages}/studentmanagement/student-overview/Studentoverview.helpers.ts (100%) rename client/src/{view => pages}/studentmanagement/student-overview/Studentoverview.tsx (100%) rename client/src/{view => pages}/studentmanagement/student-overview/components/StudenRow.helpers.ts (100%) rename client/src/{view => pages}/studentmanagement/student-overview/components/StudentRow.tsx (97%) rename client/src/{view => pages}/studentmanagement/student-store/StudentStore.actions.ts (100%) rename client/src/{view => pages}/studentmanagement/student-store/StudentStore.reducers.ts (100%) rename client/src/{view => pages}/studentmanagement/student-store/StudentStore.tsx (100%) rename client/src/{view => pages}/teamoverview/Teamoverview.tsx (100%) rename client/src/{view => pages}/teamoverview/components/CreateTeamDialog.tsx (100%) rename client/src/{view => pages}/tutorial-internals-management/TutorialInternalsManagement.tsx (100%) rename client/src/{view => pages}/tutorial-internals-management/components/Routes.tsx (100%) rename client/src/{view => pages}/tutorial-substitutes/SubstituteManagement.context.tsx (87%) rename client/src/{view => pages}/tutorial-substitutes/SubstituteManagement.tsx (100%) rename client/src/{view => pages}/tutorial-substitutes/SubstituteManagement.types.ts (100%) rename client/src/{view => pages}/tutorial-substitutes/components/DateBox.tsx (100%) rename client/src/{view => pages}/tutorial-substitutes/components/DateButton.tsx (100%) rename client/src/{view => pages}/tutorial-substitutes/components/ListOfTutors.tsx (100%) rename client/src/{view => pages}/tutorial-substitutes/components/SelectSubstitute.tsx (100%) rename client/src/{view => pages}/tutorial-substitutes/components/SelectedSubstituteBar.tsx (100%) rename client/src/{view => pages}/tutorialmanagement/TutorialManagement.tsx (100%) rename client/src/{view => pages}/tutorialmanagement/components/TutorialTableRow.tsx (100%) rename client/src/{view => pages}/usermanagement/UserManagement.tsx (100%) rename client/src/{view => pages}/usermanagement/components/UserTableRow.tsx (100%) diff --git a/client/src/components/ContextWrapper.tsx b/client/src/components/ContextWrapper.tsx index bf0087153..acc945c54 100644 --- a/client/src/components/ContextWrapper.tsx +++ b/client/src/components/ContextWrapper.tsx @@ -80,19 +80,19 @@ function handleUserConfirmation(message: string, callback: (ok: boolean) => void function ContextWrapper({ children, Router }: PropsWithChildren): JSX.Element { return ( - - - - + + + + {children} - - - - + + + + ); } diff --git a/client/src/components/attendance-controls/AttendanceControls.tsx b/client/src/components/attendance-controls/AttendanceControls.tsx index 0735e1c1a..6cbba47bc 100644 --- a/client/src/components/attendance-controls/AttendanceControls.tsx +++ b/client/src/components/attendance-controls/AttendanceControls.tsx @@ -1,7 +1,7 @@ import { Box, BoxProps } from '@material-ui/core'; import React from 'react'; import { AttendanceState, IAttendance } from 'shared/model/Attendance'; -import AttendanceButton from '../../view/attendance/components/AttendanceButton'; +import AttendanceButton from '../../pages/attendance/components/AttendanceButton'; import AttendanceNotePopper, { NoteFormCallback } from './components/AttendanceNotePopper'; export interface AttendanceControlsProps extends BoxProps { diff --git a/client/src/components/stepper-with-buttons/context/StepperContext.tsx b/client/src/components/stepper-with-buttons/context/StepperContext.tsx index 2da9f3436..583fdec2c 100644 --- a/client/src/components/stepper-with-buttons/context/StepperContext.tsx +++ b/client/src/components/stepper-with-buttons/context/StepperContext.tsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react'; -import { notInitializied } from '../../../util/throwFunctions'; +import { throwContextNotInitialized } from '../../../util/throwFunctions'; export interface NextStepInformation { goToNext: boolean; @@ -35,13 +35,13 @@ export const StepperContext = React.createContext({ isWaitingOnNextCallback: false, isNextDisabled: false, steps: [], - setWaitingOnNextCallback: notInitializied('StepperContext'), - prevStep: notInitializied('StepperContext'), - nextStep: notInitializied('StepperContext'), - setNextDisabled: notInitializied('StepperContext'), - setNextCallback: notInitializied('StepperContext'), - removeNextCallback: notInitializied('StepperContext'), - getNextCallback: notInitializied('StepperContext'), + setWaitingOnNextCallback: throwContextNotInitialized('StepperContext'), + prevStep: throwContextNotInitialized('StepperContext'), + nextStep: throwContextNotInitialized('StepperContext'), + setNextDisabled: throwContextNotInitialized('StepperContext'), + setNextCallback: throwContextNotInitialized('StepperContext'), + removeNextCallback: throwContextNotInitialized('StepperContext'), + getNextCallback: throwContextNotInitialized('StepperContext'), }); export function useStepper(): Omit { diff --git a/client/src/index.tsx b/client/src/index.tsx index 02be12ac3..bb206ee6f 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,10 +1,10 @@ -import 'reflect-metadata'; import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; +import 'reflect-metadata'; import ContextWrapper from './components/ContextWrapper'; +import App from './pages/App'; import * as serviceWorker from './serviceWorker'; -import App from './view/App'; ReactDOM.render( diff --git a/client/src/view/App.tsx b/client/src/pages/App.tsx similarity index 100% rename from client/src/view/App.tsx rename to client/src/pages/App.tsx diff --git a/client/src/view/AppBar.tsx b/client/src/pages/AppBar.tsx similarity index 100% rename from client/src/view/AppBar.tsx rename to client/src/pages/AppBar.tsx diff --git a/client/src/view/Login.tsx b/client/src/pages/Login.tsx similarity index 100% rename from client/src/view/Login.tsx rename to client/src/pages/Login.tsx diff --git a/client/src/view/attendance/AttendanceAdminView.tsx b/client/src/pages/attendance/AttendanceAdminView.tsx similarity index 100% rename from client/src/view/attendance/AttendanceAdminView.tsx rename to client/src/pages/attendance/AttendanceAdminView.tsx diff --git a/client/src/view/attendance/AttendanceManager.tsx b/client/src/pages/attendance/AttendanceManager.tsx similarity index 97% rename from client/src/view/attendance/AttendanceManager.tsx rename to client/src/pages/attendance/AttendanceManager.tsx index c452aa4cc..d7ade07c2 100644 --- a/client/src/view/attendance/AttendanceManager.tsx +++ b/client/src/pages/attendance/AttendanceManager.tsx @@ -5,7 +5,6 @@ import { DateTime } from 'luxon'; import { useSnackbar } from 'notistack'; import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'; import { AttendanceState, IAttendance, IAttendanceDTO } from 'shared/model/Attendance'; -import { Role } from 'shared/model/Role'; import { StudentStatus } from 'shared/model/Student'; import { NoteFormCallback } from '../../components/attendance-controls/components/AttendanceNotePopper'; import CustomSelect from '../../components/CustomSelect'; @@ -115,7 +114,7 @@ function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element const logger = useLogger('AttendanceManager'); const { userData } = useLogin(); - const { settings } = useSettings(); + const { canStudentBeExcused } = useSettings(); const { enqueueSnackbar } = useSnackbar(); const [isLoading, setIsLoading] = useState(false); @@ -137,13 +136,6 @@ function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element userData, tutorialFromProps, ]); - const canStudentBeExcused = useMemo(() => { - if (settings.canTutorExcuseStudents) { - return true; - } - - return !!userData && userData.roles.includes(Role.ADMIN); - }, [userData, settings.canTutorExcuseStudents]); useEffect(() => { if (!!tutorialFromProps) { @@ -406,7 +398,7 @@ function AttendanceManager({ tutorial: tutorialFromProps }: Props): JSX.Element onAttendanceSelection={(state) => handleStudentAttendanceChange(student, state)} onNoteSave={handleStudentNoteChange(student)} onCakeCountChanged={handleCakeCountChange(student)} - canBeExcused={canStudentBeExcused} + canBeExcused={canStudentBeExcused()} /> ); }} diff --git a/client/src/view/attendance/AttendanceView.tsx b/client/src/pages/attendance/AttendanceView.tsx similarity index 100% rename from client/src/view/attendance/AttendanceView.tsx rename to client/src/pages/attendance/AttendanceView.tsx diff --git a/client/src/view/attendance/components/AttendanceButton.tsx b/client/src/pages/attendance/components/AttendanceButton.tsx similarity index 100% rename from client/src/view/attendance/components/AttendanceButton.tsx rename to client/src/pages/attendance/components/AttendanceButton.tsx diff --git a/client/src/view/attendance/components/CakeCount.tsx b/client/src/pages/attendance/components/CakeCount.tsx similarity index 100% rename from client/src/view/attendance/components/CakeCount.tsx rename to client/src/pages/attendance/components/CakeCount.tsx diff --git a/client/src/view/attendance/components/StudentsAttendanceRow.tsx b/client/src/pages/attendance/components/StudentsAttendanceRow.tsx similarity index 100% rename from client/src/view/attendance/components/StudentsAttendanceRow.tsx rename to client/src/pages/attendance/components/StudentsAttendanceRow.tsx diff --git a/client/src/view/criteria-info-view/CriteriaInfoView.tsx b/client/src/pages/criteria-info-view/CriteriaInfoView.tsx similarity index 100% rename from client/src/view/criteria-info-view/CriteriaInfoView.tsx rename to client/src/pages/criteria-info-view/CriteriaInfoView.tsx diff --git a/client/src/view/dashboard/Dashboard.tsx b/client/src/pages/dashboard/Dashboard.tsx similarity index 100% rename from client/src/view/dashboard/Dashboard.tsx rename to client/src/pages/dashboard/Dashboard.tsx diff --git a/client/src/view/dashboard/components/AdminStatsCard.tsx b/client/src/pages/dashboard/components/AdminStatsCard.tsx similarity index 100% rename from client/src/view/dashboard/components/AdminStatsCard.tsx rename to client/src/pages/dashboard/components/AdminStatsCard.tsx diff --git a/client/src/view/dashboard/components/AllTutorialStatistics.tsx b/client/src/pages/dashboard/components/AllTutorialStatistics.tsx similarity index 100% rename from client/src/view/dashboard/components/AllTutorialStatistics.tsx rename to client/src/pages/dashboard/components/AllTutorialStatistics.tsx diff --git a/client/src/view/dashboard/components/AspectRatio.tsx b/client/src/pages/dashboard/components/AspectRatio.tsx similarity index 100% rename from client/src/view/dashboard/components/AspectRatio.tsx rename to client/src/pages/dashboard/components/AspectRatio.tsx diff --git a/client/src/view/dashboard/components/ScheinCrtieriaStatsCard.tsx b/client/src/pages/dashboard/components/ScheinCrtieriaStatsCard.tsx similarity index 100% rename from client/src/view/dashboard/components/ScheinCrtieriaStatsCard.tsx rename to client/src/pages/dashboard/components/ScheinCrtieriaStatsCard.tsx diff --git a/client/src/view/dashboard/components/ScheinPassedStatsCard.tsx b/client/src/pages/dashboard/components/ScheinPassedStatsCard.tsx similarity index 100% rename from client/src/view/dashboard/components/ScheinPassedStatsCard.tsx rename to client/src/pages/dashboard/components/ScheinPassedStatsCard.tsx diff --git a/client/src/view/dashboard/components/TutorialStatistics.tsx b/client/src/pages/dashboard/components/TutorialStatistics.tsx similarity index 100% rename from client/src/view/dashboard/components/TutorialStatistics.tsx rename to client/src/pages/dashboard/components/TutorialStatistics.tsx diff --git a/client/src/view/dashboard/components/TutorialStatsCard.tsx b/client/src/pages/dashboard/components/TutorialStatsCard.tsx similarity index 100% rename from client/src/view/dashboard/components/TutorialStatsCard.tsx rename to client/src/pages/dashboard/components/TutorialStatsCard.tsx diff --git a/client/src/view/generate-tutorials/GenerateTutorials.tsx b/client/src/pages/generate-tutorials/GenerateTutorials.tsx similarity index 100% rename from client/src/view/generate-tutorials/GenerateTutorials.tsx rename to client/src/pages/generate-tutorials/GenerateTutorials.tsx diff --git a/client/src/view/generate-tutorials/GenerateTutorials.validation.tsx b/client/src/pages/generate-tutorials/GenerateTutorials.validation.tsx similarity index 100% rename from client/src/view/generate-tutorials/GenerateTutorials.validation.tsx rename to client/src/pages/generate-tutorials/GenerateTutorials.validation.tsx diff --git a/client/src/view/generate-tutorials/components/excluded-dates/ExcludedDateBox.tsx b/client/src/pages/generate-tutorials/components/excluded-dates/ExcludedDateBox.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/excluded-dates/ExcludedDateBox.tsx rename to client/src/pages/generate-tutorials/components/excluded-dates/ExcludedDateBox.tsx diff --git a/client/src/view/generate-tutorials/components/excluded-dates/ExcludedDateDialog.tsx b/client/src/pages/generate-tutorials/components/excluded-dates/ExcludedDateDialog.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/excluded-dates/ExcludedDateDialog.tsx rename to client/src/pages/generate-tutorials/components/excluded-dates/ExcludedDateDialog.tsx diff --git a/client/src/view/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx b/client/src/pages/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx rename to client/src/pages/generate-tutorials/components/excluded-dates/FormikExcludedDates.tsx diff --git a/client/src/view/generate-tutorials/components/weekday-slots/AddSlotForm.tsx b/client/src/pages/generate-tutorials/components/weekday-slots/AddSlotForm.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/weekday-slots/AddSlotForm.tsx rename to client/src/pages/generate-tutorials/components/weekday-slots/AddSlotForm.tsx diff --git a/client/src/view/generate-tutorials/components/weekday-slots/FormikWeekdaySlot.tsx b/client/src/pages/generate-tutorials/components/weekday-slots/FormikWeekdaySlot.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/weekday-slots/FormikWeekdaySlot.tsx rename to client/src/pages/generate-tutorials/components/weekday-slots/FormikWeekdaySlot.tsx diff --git a/client/src/view/generate-tutorials/components/weekday-slots/IconForTab.tsx b/client/src/pages/generate-tutorials/components/weekday-slots/IconForTab.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/weekday-slots/IconForTab.tsx rename to client/src/pages/generate-tutorials/components/weekday-slots/IconForTab.tsx diff --git a/client/src/view/generate-tutorials/components/weekday-slots/WeekdayBox.tsx b/client/src/pages/generate-tutorials/components/weekday-slots/WeekdayBox.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/weekday-slots/WeekdayBox.tsx rename to client/src/pages/generate-tutorials/components/weekday-slots/WeekdayBox.tsx diff --git a/client/src/view/generate-tutorials/components/weekday-slots/WeekdayTabs.tsx b/client/src/pages/generate-tutorials/components/weekday-slots/WeekdayTabs.tsx similarity index 100% rename from client/src/view/generate-tutorials/components/weekday-slots/WeekdayTabs.tsx rename to client/src/pages/generate-tutorials/components/weekday-slots/WeekdayTabs.tsx diff --git a/client/src/view/import-data/ImportUsers.context.tsx b/client/src/pages/import-data/ImportUsers.context.tsx similarity index 91% rename from client/src/view/import-data/ImportUsers.context.tsx rename to client/src/pages/import-data/ImportUsers.context.tsx index c932014a0..de9d6d5db 100644 --- a/client/src/view/import-data/ImportUsers.context.tsx +++ b/client/src/pages/import-data/ImportUsers.context.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { getAllTutorials } from '../../hooks/fetching/Tutorial'; import { Tutorial } from '../../model/Tutorial'; import { RequireChildrenProp } from '../../typings/RequireChildrenProp'; -import { notInitializied } from '../../util/throwFunctions'; +import { throwContextNotInitialized } from '../../util/throwFunctions'; interface ParsedCSVDataRow { [header: string]: string; @@ -65,9 +65,9 @@ const DataContext = React.createContext({ data: { headers: [], rows: [] }, mappedColumns: initialMappedColumns, csvFormData: undefined, - setData: notInitializied('ImportUsersContext'), - setMappedColumns: notInitializied('ImportUsersContext'), - setCSVFormData: notInitializied('ImportUsersContext'), + setData: throwContextNotInitialized('ImportUsersContext'), + setMappedColumns: throwContextNotInitialized('ImportUsersContext'), + setCSVFormData: throwContextNotInitialized('ImportUsersContext'), }); function convertParsedToInternalCSV(data: ParsedCSVData): CSVData { diff --git a/client/src/view/import-data/ImportUsers.tsx b/client/src/pages/import-data/ImportUsers.tsx similarity index 100% rename from client/src/view/import-data/ImportUsers.tsx rename to client/src/pages/import-data/ImportUsers.tsx diff --git a/client/src/view/import-data/adjust-data-form/AdjustImportedUserDataForm.tsx b/client/src/pages/import-data/adjust-data-form/AdjustImportedUserDataForm.tsx similarity index 100% rename from client/src/view/import-data/adjust-data-form/AdjustImportedUserDataForm.tsx rename to client/src/pages/import-data/adjust-data-form/AdjustImportedUserDataForm.tsx diff --git a/client/src/view/import-data/adjust-data-form/components/UserDataBox.helpers.ts b/client/src/pages/import-data/adjust-data-form/components/UserDataBox.helpers.ts similarity index 100% rename from client/src/view/import-data/adjust-data-form/components/UserDataBox.helpers.ts rename to client/src/pages/import-data/adjust-data-form/components/UserDataBox.helpers.ts diff --git a/client/src/view/import-data/adjust-data-form/components/UserDataBox.tsx b/client/src/pages/import-data/adjust-data-form/components/UserDataBox.tsx similarity index 100% rename from client/src/view/import-data/adjust-data-form/components/UserDataBox.tsx rename to client/src/pages/import-data/adjust-data-form/components/UserDataBox.tsx diff --git a/client/src/view/import-data/adjust-data-form/components/UserDataRow.tsx b/client/src/pages/import-data/adjust-data-form/components/UserDataRow.tsx similarity index 100% rename from client/src/view/import-data/adjust-data-form/components/UserDataRow.tsx rename to client/src/pages/import-data/adjust-data-form/components/UserDataRow.tsx diff --git a/client/src/view/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialog.tsx b/client/src/pages/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialog.tsx similarity index 100% rename from client/src/view/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialog.tsx rename to client/src/pages/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialog.tsx diff --git a/client/src/view/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialogContent.tsx b/client/src/pages/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialogContent.tsx similarity index 100% rename from client/src/view/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialogContent.tsx rename to client/src/pages/import-data/adjust-data-form/components/edit-user-dialog/EditUserDialogContent.tsx diff --git a/client/src/view/import-data/import-csv/ImportUserCSV.tsx b/client/src/pages/import-data/import-csv/ImportUserCSV.tsx similarity index 100% rename from client/src/view/import-data/import-csv/ImportUserCSV.tsx rename to client/src/pages/import-data/import-csv/ImportUserCSV.tsx diff --git a/client/src/view/import-data/map-columns/MapCSVColumns.tsx b/client/src/pages/import-data/map-columns/MapCSVColumns.tsx similarity index 100% rename from client/src/view/import-data/map-columns/MapCSVColumns.tsx rename to client/src/pages/import-data/map-columns/MapCSVColumns.tsx diff --git a/client/src/view/management/components/InfoTable.tsx b/client/src/pages/management/components/InfoTable.tsx similarity index 100% rename from client/src/view/management/components/InfoTable.tsx rename to client/src/pages/management/components/InfoTable.tsx diff --git a/client/src/view/management/components/ScheinCriteriaStatusTable.tsx b/client/src/pages/management/components/ScheinCriteriaStatusTable.tsx similarity index 100% rename from client/src/view/management/components/ScheinCriteriaStatusTable.tsx rename to client/src/pages/management/components/ScheinCriteriaStatusTable.tsx diff --git a/client/src/view/management/components/StatusProgress.tsx b/client/src/pages/management/components/StatusProgress.tsx similarity index 100% rename from client/src/view/management/components/StatusProgress.tsx rename to client/src/pages/management/components/StatusProgress.tsx diff --git a/client/src/view/points-scheinexam/enter-form/EnterScheinexamPoints.tsx b/client/src/pages/points-scheinexam/enter-form/EnterScheinexamPoints.tsx similarity index 100% rename from client/src/view/points-scheinexam/enter-form/EnterScheinexamPoints.tsx rename to client/src/pages/points-scheinexam/enter-form/EnterScheinexamPoints.tsx diff --git a/client/src/view/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx b/client/src/pages/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx similarity index 100% rename from client/src/view/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx rename to client/src/pages/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx diff --git a/client/src/view/points-scheinexam/overview/ScheinexamPointsOverview.tsx b/client/src/pages/points-scheinexam/overview/ScheinexamPointsOverview.tsx similarity index 100% rename from client/src/view/points-scheinexam/overview/ScheinexamPointsOverview.tsx rename to client/src/pages/points-scheinexam/overview/ScheinexamPointsOverview.tsx diff --git a/client/src/view/points-scheinexam/overview/components/StudentCard.tsx b/client/src/pages/points-scheinexam/overview/components/StudentCard.tsx similarity index 100% rename from client/src/view/points-scheinexam/overview/components/StudentCard.tsx rename to client/src/pages/points-scheinexam/overview/components/StudentCard.tsx diff --git a/client/src/view/points-scheinexam/overview/components/StudentCardList.tsx b/client/src/pages/points-scheinexam/overview/components/StudentCardList.tsx similarity index 100% rename from client/src/view/points-scheinexam/overview/components/StudentCardList.tsx rename to client/src/pages/points-scheinexam/overview/components/StudentCardList.tsx diff --git a/client/src/view/points-sheet/enter-form/EnterPoints.helpers.ts b/client/src/pages/points-sheet/enter-form/EnterPoints.helpers.ts similarity index 100% rename from client/src/view/points-sheet/enter-form/EnterPoints.helpers.ts rename to client/src/pages/points-sheet/enter-form/EnterPoints.helpers.ts diff --git a/client/src/view/points-sheet/enter-form/EnterPoints.tsx b/client/src/pages/points-sheet/enter-form/EnterPoints.tsx similarity index 100% rename from client/src/view/points-sheet/enter-form/EnterPoints.tsx rename to client/src/pages/points-sheet/enter-form/EnterPoints.tsx diff --git a/client/src/view/points-sheet/enter-form/EnterStudentPoints.tsx b/client/src/pages/points-sheet/enter-form/EnterStudentPoints.tsx similarity index 100% rename from client/src/view/points-sheet/enter-form/EnterStudentPoints.tsx rename to client/src/pages/points-sheet/enter-form/EnterStudentPoints.tsx diff --git a/client/src/view/points-sheet/enter-form/EnterTeamPoints.tsx b/client/src/pages/points-sheet/enter-form/EnterTeamPoints.tsx similarity index 100% rename from client/src/view/points-sheet/enter-form/EnterTeamPoints.tsx rename to client/src/pages/points-sheet/enter-form/EnterTeamPoints.tsx diff --git a/client/src/view/points-sheet/enter-form/components/EnterPointsForm.helpers.ts b/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.helpers.ts similarity index 100% rename from client/src/view/points-sheet/enter-form/components/EnterPointsForm.helpers.ts rename to client/src/pages/points-sheet/enter-form/components/EnterPointsForm.helpers.ts diff --git a/client/src/view/points-sheet/enter-form/components/EnterPointsForm.tsx b/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.tsx similarity index 100% rename from client/src/view/points-sheet/enter-form/components/EnterPointsForm.tsx rename to client/src/pages/points-sheet/enter-form/components/EnterPointsForm.tsx diff --git a/client/src/view/points-sheet/enter-form/components/ExerciseBox.tsx b/client/src/pages/points-sheet/enter-form/components/ExerciseBox.tsx similarity index 100% rename from client/src/view/points-sheet/enter-form/components/ExerciseBox.tsx rename to client/src/pages/points-sheet/enter-form/components/ExerciseBox.tsx diff --git a/client/src/view/points-sheet/overview/PointsOverview.tsx b/client/src/pages/points-sheet/overview/PointsOverview.tsx similarity index 100% rename from client/src/view/points-sheet/overview/PointsOverview.tsx rename to client/src/pages/points-sheet/overview/PointsOverview.tsx diff --git a/client/src/view/points-sheet/overview/components/TeamCard.tsx b/client/src/pages/points-sheet/overview/components/TeamCard.tsx similarity index 100% rename from client/src/view/points-sheet/overview/components/TeamCard.tsx rename to client/src/pages/points-sheet/overview/components/TeamCard.tsx diff --git a/client/src/view/points-sheet/overview/components/TeamCardList.tsx b/client/src/pages/points-sheet/overview/components/TeamCardList.tsx similarity index 100% rename from client/src/view/points-sheet/overview/components/TeamCardList.tsx rename to client/src/pages/points-sheet/overview/components/TeamCardList.tsx diff --git a/client/src/view/points-sheet/util/helper.ts b/client/src/pages/points-sheet/util/helper.ts similarity index 100% rename from client/src/view/points-sheet/util/helper.ts rename to client/src/pages/points-sheet/util/helper.ts diff --git a/client/src/view/presentation-points/PresentationPoints.tsx b/client/src/pages/presentation-points/PresentationPoints.tsx similarity index 100% rename from client/src/view/presentation-points/PresentationPoints.tsx rename to client/src/pages/presentation-points/PresentationPoints.tsx diff --git a/client/src/view/presentation-points/components/PresentationList.tsx b/client/src/pages/presentation-points/components/PresentationList.tsx similarity index 100% rename from client/src/view/presentation-points/components/PresentationList.tsx rename to client/src/pages/presentation-points/components/PresentationList.tsx diff --git a/client/src/view/scheincriteriamanagement/ScheinCriteriaManagement.tsx b/client/src/pages/scheincriteriamanagement/ScheinCriteriaManagement.tsx similarity index 100% rename from client/src/view/scheincriteriamanagement/ScheinCriteriaManagement.tsx rename to client/src/pages/scheincriteriamanagement/ScheinCriteriaManagement.tsx diff --git a/client/src/view/scheincriteriamanagement/components/ScheinCriteriaRow.tsx b/client/src/pages/scheincriteriamanagement/components/ScheinCriteriaRow.tsx similarity index 100% rename from client/src/view/scheincriteriamanagement/components/ScheinCriteriaRow.tsx rename to client/src/pages/scheincriteriamanagement/components/ScheinCriteriaRow.tsx diff --git a/client/src/view/scheinexam-management/ScheinExamManagement.tsx b/client/src/pages/scheinexam-management/ScheinExamManagement.tsx similarity index 100% rename from client/src/view/scheinexam-management/ScheinExamManagement.tsx rename to client/src/pages/scheinexam-management/ScheinExamManagement.tsx diff --git a/client/src/view/scheinexam-management/components/ScheinExamRow.tsx b/client/src/pages/scheinexam-management/components/ScheinExamRow.tsx similarity index 100% rename from client/src/view/scheinexam-management/components/ScheinExamRow.tsx rename to client/src/pages/scheinexam-management/components/ScheinExamRow.tsx diff --git a/client/src/view/sheetmanagement/SheetManagement.tsx b/client/src/pages/sheetmanagement/SheetManagement.tsx similarity index 100% rename from client/src/view/sheetmanagement/SheetManagement.tsx rename to client/src/pages/sheetmanagement/SheetManagement.tsx diff --git a/client/src/view/sheetmanagement/components/SheetRow.tsx b/client/src/pages/sheetmanagement/components/SheetRow.tsx similarity index 100% rename from client/src/view/sheetmanagement/components/SheetRow.tsx rename to client/src/pages/sheetmanagement/components/SheetRow.tsx diff --git a/client/src/view/studentmanagement/student-info/StudentInfo.tsx b/client/src/pages/student-info/StudentInfo.tsx similarity index 88% rename from client/src/view/studentmanagement/student-info/StudentInfo.tsx rename to client/src/pages/student-info/StudentInfo.tsx index 99a887637..4090dce59 100644 --- a/client/src/view/studentmanagement/student-info/StudentInfo.tsx +++ b/client/src/pages/student-info/StudentInfo.tsx @@ -6,20 +6,20 @@ import { useParams } from 'react-router'; import { AttendanceState, IAttendance, IAttendanceDTO } from 'shared/model/Attendance'; import { ScheinCriteriaSummary } from 'shared/model/ScheinCriteria'; import { getNameOfEntity } from 'shared/util/helpers'; -import BackButton from '../../../components/back-button/BackButton'; -import Placeholder from '../../../components/Placeholder'; -import TabPanel from '../../../components/TabPanel'; -import { getScheinCriteriaSummaryOfStudent } from '../../../hooks/fetching/Scheincriteria'; -import { getAllScheinExams } from '../../../hooks/fetching/ScheinExam'; -import { getAllSheets } from '../../../hooks/fetching/Sheet'; -import { getStudent, setAttendanceOfStudent } from '../../../hooks/fetching/Student'; -import { getTutorial } from '../../../hooks/fetching/Tutorial'; -import { useCustomSnackbar } from '../../../hooks/snackbar/useCustomSnackbar'; -import { Scheinexam } from '../../../model/Scheinexam'; -import { Sheet } from '../../../model/Sheet'; -import { Student } from '../../../model/Student'; -import { Tutorial } from '../../../model/Tutorial'; -import { ROUTES } from '../../../routes/Routing.routes'; +import BackButton from '../../components/back-button/BackButton'; +import Placeholder from '../../components/Placeholder'; +import TabPanel from '../../components/TabPanel'; +import { getScheinCriteriaSummaryOfStudent } from '../../hooks/fetching/Scheincriteria'; +import { getAllScheinExams } from '../../hooks/fetching/ScheinExam'; +import { getAllSheets } from '../../hooks/fetching/Sheet'; +import { getStudent, setAttendanceOfStudent } from '../../hooks/fetching/Student'; +import { getTutorial } from '../../hooks/fetching/Tutorial'; +import { useCustomSnackbar } from '../../hooks/snackbar/useCustomSnackbar'; +import { Scheinexam } from '../../model/Scheinexam'; +import { Sheet } from '../../model/Sheet'; +import { Student } from '../../model/Student'; +import { Tutorial } from '../../model/Tutorial'; +import { ROUTES } from '../../routes/Routing.routes'; import AttendanceInformation from './components/AttendanceInformation'; import CriteriaCharts from './components/CriteriaCharts'; import EvaluationInformation from './components/EvaluationInformation'; diff --git a/client/src/view/studentmanagement/student-info/components/AttendanceInformation.tsx b/client/src/pages/student-info/components/AttendanceInformation.tsx similarity index 85% rename from client/src/view/studentmanagement/student-info/components/AttendanceInformation.tsx rename to client/src/pages/student-info/components/AttendanceInformation.tsx index 8b05afe24..e0429e05e 100644 --- a/client/src/view/studentmanagement/student-info/components/AttendanceInformation.tsx +++ b/client/src/pages/student-info/components/AttendanceInformation.tsx @@ -2,10 +2,10 @@ import { Table, TableBody, TableCell, TableHead, TableProps, TableRow } from '@m import { DateTime } from 'luxon'; import React from 'react'; import { AttendanceState, IAttendance } from 'shared/model/Attendance'; -import AttendanceControls from '../../../../components/attendance-controls/AttendanceControls'; -import { Student } from '../../../../model/Student'; -import { Tutorial } from '../../../../model/Tutorial'; -import { parseDateToMapKey } from '../../../../util/helperFunctions'; +import AttendanceControls from '../../../components/attendance-controls/AttendanceControls'; +import { Student } from '../../../model/Student'; +import { Tutorial } from '../../../model/Tutorial'; +import { parseDateToMapKey } from '../../../util/helperFunctions'; interface Props extends TableProps { student: Student; @@ -21,6 +21,7 @@ function AttendanceInformation({ onNoteChange, ...props }: Props): JSX.Element { + return ( diff --git a/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx b/client/src/pages/student-info/components/CriteriaCharts.tsx similarity index 95% rename from client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx rename to client/src/pages/student-info/components/CriteriaCharts.tsx index 43d31415b..d58b8bd94 100644 --- a/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx +++ b/client/src/pages/student-info/components/CriteriaCharts.tsx @@ -2,7 +2,7 @@ import { Grid, GridProps } from '@material-ui/core'; import { useTheme } from '@material-ui/core/styles'; import React from 'react'; import { ScheinCriteriaSummary } from 'shared/model/ScheinCriteria'; -import ChartPaper from '../../../../components/info-paper/ChartPaper'; +import ChartPaper from '../../../components/info-paper/ChartPaper'; interface Props extends GridProps { firstCard?: React.ReactNode; diff --git a/client/src/view/studentmanagement/student-info/components/EvaluationInformation.tsx b/client/src/pages/student-info/components/EvaluationInformation.tsx similarity index 84% rename from client/src/view/studentmanagement/student-info/components/EvaluationInformation.tsx rename to client/src/pages/student-info/components/EvaluationInformation.tsx index 6775c03e0..4eebf8592 100644 --- a/client/src/view/studentmanagement/student-info/components/EvaluationInformation.tsx +++ b/client/src/pages/student-info/components/EvaluationInformation.tsx @@ -2,15 +2,15 @@ import { Box, BoxProps, Typography } from '@material-ui/core'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import { useSnackbar } from 'notistack'; import React, { useEffect, useState } from 'react'; -import CustomSelect, { OnChangeHandler } from '../../../../components/CustomSelect'; -import LoadingSpinner from '../../../../components/loading/LoadingSpinner'; -import Markdown from '../../../../components/Markdown'; -import Placeholder from '../../../../components/Placeholder'; -import PointsTable from '../../../../components/points-table/PointsTable'; -import { getStudentCorrectionCommentMarkdown } from '../../../../hooks/fetching/Files'; -import { Grading } from '../../../../model/Grading'; -import { Sheet } from '../../../../model/Sheet'; -import { Student } from '../../../../model/Student'; +import CustomSelect, { OnChangeHandler } from '../../../components/CustomSelect'; +import LoadingSpinner from '../../../components/loading/LoadingSpinner'; +import Markdown from '../../../components/Markdown'; +import Placeholder from '../../../components/Placeholder'; +import PointsTable from '../../../components/points-table/PointsTable'; +import { getStudentCorrectionCommentMarkdown } from '../../../hooks/fetching/Files'; +import { Grading } from '../../../model/Grading'; +import { Sheet } from '../../../model/Sheet'; +import { Student } from '../../../model/Student'; const useStyles = makeStyles((theme) => createStyles({ diff --git a/client/src/view/studentmanagement/student-info/components/ScheinExamInformation.tsx b/client/src/pages/student-info/components/ScheinExamInformation.tsx similarity index 85% rename from client/src/view/studentmanagement/student-info/components/ScheinExamInformation.tsx rename to client/src/pages/student-info/components/ScheinExamInformation.tsx index 4b73cf7a1..fbc487758 100644 --- a/client/src/view/studentmanagement/student-info/components/ScheinExamInformation.tsx +++ b/client/src/pages/student-info/components/ScheinExamInformation.tsx @@ -1,12 +1,12 @@ import { Box, BoxProps, Typography } from '@material-ui/core'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import React, { useEffect, useState } from 'react'; -import CustomSelect, { OnChangeHandler } from '../../../../components/CustomSelect'; -import Placeholder from '../../../../components/Placeholder'; -import PointsTable from '../../../../components/points-table/PointsTable'; -import { Grading } from '../../../../model/Grading'; -import { Scheinexam } from '../../../../model/Scheinexam'; -import { Student } from '../../../../model/Student'; +import CustomSelect, { OnChangeHandler } from '../../../components/CustomSelect'; +import Placeholder from '../../../components/Placeholder'; +import PointsTable from '../../../components/points-table/PointsTable'; +import { Grading } from '../../../model/Grading'; +import { Scheinexam } from '../../../model/Scheinexam'; +import { Student } from '../../../model/Student'; const useStyles = makeStyles((theme) => createStyles({ diff --git a/client/src/view/studentmanagement/student-info/components/ScheinStatusBox.tsx b/client/src/pages/student-info/components/ScheinStatusBox.tsx similarity index 100% rename from client/src/view/studentmanagement/student-info/components/ScheinStatusBox.tsx rename to client/src/pages/student-info/components/ScheinStatusBox.tsx diff --git a/client/src/view/studentmanagement/student-info/components/StudentDetails.tsx b/client/src/pages/student-info/components/StudentDetails.tsx similarity index 89% rename from client/src/view/studentmanagement/student-info/components/StudentDetails.tsx rename to client/src/pages/student-info/components/StudentDetails.tsx index d92915447..b3ea8d835 100644 --- a/client/src/view/studentmanagement/student-info/components/StudentDetails.tsx +++ b/client/src/pages/student-info/components/StudentDetails.tsx @@ -1,8 +1,8 @@ import { Table, TableBody, TableCell, TableRow } from '@material-ui/core'; import React from 'react'; import { StudentStatus } from 'shared/model/Student'; -import InfoPaper, { InfoPaperProps } from '../../../../components/info-paper/InfoPaper'; -import { Student } from '../../../../model/Student'; +import InfoPaper, { InfoPaperProps } from '../../../components/info-paper/InfoPaper'; +import { Student } from '../../../model/Student'; interface Props extends Omit { student: Student; diff --git a/client/src/view/studentmanagement/AllStudentsAdminView.tsx b/client/src/pages/studentmanagement/AllStudentsAdminView.tsx similarity index 100% rename from client/src/view/studentmanagement/AllStudentsAdminView.tsx rename to client/src/pages/studentmanagement/AllStudentsAdminView.tsx diff --git a/client/src/view/studentmanagement/TutorStudentmanagement.tsx b/client/src/pages/studentmanagement/TutorStudentmanagement.tsx similarity index 93% rename from client/src/view/studentmanagement/TutorStudentmanagement.tsx rename to client/src/pages/studentmanagement/TutorStudentmanagement.tsx index 7743bc5ee..62e2eb794 100644 --- a/client/src/view/studentmanagement/TutorStudentmanagement.tsx +++ b/client/src/pages/studentmanagement/TutorStudentmanagement.tsx @@ -2,7 +2,7 @@ import { Theme } from '@material-ui/core'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import _ from 'lodash'; import React from 'react'; -import { RouteComponentProps, useParams } from 'react-router'; +import { useParams } from 'react-router'; import { getNameOfEntity } from 'shared/util/helpers'; import { Student } from '../../model/Student'; import Studentoverview from './student-overview/Studentoverview'; @@ -29,8 +29,6 @@ interface Params { tutorialId: string; } -type PropType = RouteComponentProps; - function unifyFilterableText(text: string): string { return _.deburr(text).toLowerCase(); } diff --git a/client/src/view/studentmanagement/student-overview/Studentoverview.helpers.ts b/client/src/pages/studentmanagement/student-overview/Studentoverview.helpers.ts similarity index 100% rename from client/src/view/studentmanagement/student-overview/Studentoverview.helpers.ts rename to client/src/pages/studentmanagement/student-overview/Studentoverview.helpers.ts diff --git a/client/src/view/studentmanagement/student-overview/Studentoverview.tsx b/client/src/pages/studentmanagement/student-overview/Studentoverview.tsx similarity index 100% rename from client/src/view/studentmanagement/student-overview/Studentoverview.tsx rename to client/src/pages/studentmanagement/student-overview/Studentoverview.tsx diff --git a/client/src/view/studentmanagement/student-overview/components/StudenRow.helpers.ts b/client/src/pages/studentmanagement/student-overview/components/StudenRow.helpers.ts similarity index 100% rename from client/src/view/studentmanagement/student-overview/components/StudenRow.helpers.ts rename to client/src/pages/studentmanagement/student-overview/components/StudenRow.helpers.ts diff --git a/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx b/client/src/pages/studentmanagement/student-overview/components/StudentRow.tsx similarity index 97% rename from client/src/view/studentmanagement/student-overview/components/StudentRow.tsx rename to client/src/pages/studentmanagement/student-overview/components/StudentRow.tsx index 78130b027..911f0a8f2 100644 --- a/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx +++ b/client/src/pages/studentmanagement/student-overview/components/StudentRow.tsx @@ -15,7 +15,7 @@ import PaperTableRow, { PaperTableRowProps } from '../../../../components/PaperT import StudentAvatar from '../../../../components/student-icon/StudentAvatar'; import { Student } from '../../../../model/Student'; import { ROUTES } from '../../../../routes/Routing.routes'; -import ScheinStatusBox from '../../student-info/components/ScheinStatusBox'; +import ScheinStatusBox from '../../../student-info/components/ScheinStatusBox'; import { useStudentStore } from '../../student-store/StudentStore'; const useStyles = makeStyles((theme) => diff --git a/client/src/view/studentmanagement/student-store/StudentStore.actions.ts b/client/src/pages/studentmanagement/student-store/StudentStore.actions.ts similarity index 100% rename from client/src/view/studentmanagement/student-store/StudentStore.actions.ts rename to client/src/pages/studentmanagement/student-store/StudentStore.actions.ts diff --git a/client/src/view/studentmanagement/student-store/StudentStore.reducers.ts b/client/src/pages/studentmanagement/student-store/StudentStore.reducers.ts similarity index 100% rename from client/src/view/studentmanagement/student-store/StudentStore.reducers.ts rename to client/src/pages/studentmanagement/student-store/StudentStore.reducers.ts diff --git a/client/src/view/studentmanagement/student-store/StudentStore.tsx b/client/src/pages/studentmanagement/student-store/StudentStore.tsx similarity index 100% rename from client/src/view/studentmanagement/student-store/StudentStore.tsx rename to client/src/pages/studentmanagement/student-store/StudentStore.tsx diff --git a/client/src/view/teamoverview/Teamoverview.tsx b/client/src/pages/teamoverview/Teamoverview.tsx similarity index 100% rename from client/src/view/teamoverview/Teamoverview.tsx rename to client/src/pages/teamoverview/Teamoverview.tsx diff --git a/client/src/view/teamoverview/components/CreateTeamDialog.tsx b/client/src/pages/teamoverview/components/CreateTeamDialog.tsx similarity index 100% rename from client/src/view/teamoverview/components/CreateTeamDialog.tsx rename to client/src/pages/teamoverview/components/CreateTeamDialog.tsx diff --git a/client/src/view/tutorial-internals-management/TutorialInternalsManagement.tsx b/client/src/pages/tutorial-internals-management/TutorialInternalsManagement.tsx similarity index 100% rename from client/src/view/tutorial-internals-management/TutorialInternalsManagement.tsx rename to client/src/pages/tutorial-internals-management/TutorialInternalsManagement.tsx diff --git a/client/src/view/tutorial-internals-management/components/Routes.tsx b/client/src/pages/tutorial-internals-management/components/Routes.tsx similarity index 100% rename from client/src/view/tutorial-internals-management/components/Routes.tsx rename to client/src/pages/tutorial-internals-management/components/Routes.tsx diff --git a/client/src/view/tutorial-substitutes/SubstituteManagement.context.tsx b/client/src/pages/tutorial-substitutes/SubstituteManagement.context.tsx similarity index 87% rename from client/src/view/tutorial-substitutes/SubstituteManagement.context.tsx rename to client/src/pages/tutorial-substitutes/SubstituteManagement.context.tsx index 3ef55398b..b1b9f4ff4 100644 --- a/client/src/view/tutorial-substitutes/SubstituteManagement.context.tsx +++ b/client/src/pages/tutorial-substitutes/SubstituteManagement.context.tsx @@ -6,7 +6,7 @@ import { getTutorial } from '../../hooks/fetching/Tutorial'; import { getUserNames } from '../../hooks/fetching/User'; import { UseFetchState, useFetchState } from '../../hooks/useFetchState'; import { Tutorial } from '../../model/Tutorial'; -import { notInitializied } from '../../util/throwFunctions'; +import { throwContextNotInitialized } from '../../util/throwFunctions'; interface Props { tutorialId: string; @@ -28,19 +28,19 @@ export interface SubstituteManagementContextType { const ERR_NOT_INITIALIZED: UseFetchState = { error: 'Context not initialized', isLoading: false, - execute: notInitializied('SubstituteManagementContext'), + execute: throwContextNotInitialized('SubstituteManagementContext'), }; const Context = React.createContext({ tutorial: ERR_NOT_INITIALIZED, tutors: ERR_NOT_INITIALIZED, selectedDate: undefined, - setSelectedDate: notInitializied('SubstituteManagementContext'), - isSubstituteChanged: notInitializied('SubstituteManagementContext'), - getSelectedSubstitute: notInitializied('SubstituteManagementContext'), - setSelectedSubstitute: notInitializied('SubstituteManagementContext'), - removeSelectedSubstitute: notInitializied('SubstituteManagementContext'), - resetSelectedSubstitute: notInitializied('SubstituteManagementContext'), + setSelectedDate: throwContextNotInitialized('SubstituteManagementContext'), + isSubstituteChanged: throwContextNotInitialized('SubstituteManagementContext'), + getSelectedSubstitute: throwContextNotInitialized('SubstituteManagementContext'), + setSelectedSubstitute: throwContextNotInitialized('SubstituteManagementContext'), + removeSelectedSubstitute: throwContextNotInitialized('SubstituteManagementContext'), + resetSelectedSubstitute: throwContextNotInitialized('SubstituteManagementContext'), dirty: false, }); diff --git a/client/src/view/tutorial-substitutes/SubstituteManagement.tsx b/client/src/pages/tutorial-substitutes/SubstituteManagement.tsx similarity index 100% rename from client/src/view/tutorial-substitutes/SubstituteManagement.tsx rename to client/src/pages/tutorial-substitutes/SubstituteManagement.tsx diff --git a/client/src/view/tutorial-substitutes/SubstituteManagement.types.ts b/client/src/pages/tutorial-substitutes/SubstituteManagement.types.ts similarity index 100% rename from client/src/view/tutorial-substitutes/SubstituteManagement.types.ts rename to client/src/pages/tutorial-substitutes/SubstituteManagement.types.ts diff --git a/client/src/view/tutorial-substitutes/components/DateBox.tsx b/client/src/pages/tutorial-substitutes/components/DateBox.tsx similarity index 100% rename from client/src/view/tutorial-substitutes/components/DateBox.tsx rename to client/src/pages/tutorial-substitutes/components/DateBox.tsx diff --git a/client/src/view/tutorial-substitutes/components/DateButton.tsx b/client/src/pages/tutorial-substitutes/components/DateButton.tsx similarity index 100% rename from client/src/view/tutorial-substitutes/components/DateButton.tsx rename to client/src/pages/tutorial-substitutes/components/DateButton.tsx diff --git a/client/src/view/tutorial-substitutes/components/ListOfTutors.tsx b/client/src/pages/tutorial-substitutes/components/ListOfTutors.tsx similarity index 100% rename from client/src/view/tutorial-substitutes/components/ListOfTutors.tsx rename to client/src/pages/tutorial-substitutes/components/ListOfTutors.tsx diff --git a/client/src/view/tutorial-substitutes/components/SelectSubstitute.tsx b/client/src/pages/tutorial-substitutes/components/SelectSubstitute.tsx similarity index 100% rename from client/src/view/tutorial-substitutes/components/SelectSubstitute.tsx rename to client/src/pages/tutorial-substitutes/components/SelectSubstitute.tsx diff --git a/client/src/view/tutorial-substitutes/components/SelectedSubstituteBar.tsx b/client/src/pages/tutorial-substitutes/components/SelectedSubstituteBar.tsx similarity index 100% rename from client/src/view/tutorial-substitutes/components/SelectedSubstituteBar.tsx rename to client/src/pages/tutorial-substitutes/components/SelectedSubstituteBar.tsx diff --git a/client/src/view/tutorialmanagement/TutorialManagement.tsx b/client/src/pages/tutorialmanagement/TutorialManagement.tsx similarity index 100% rename from client/src/view/tutorialmanagement/TutorialManagement.tsx rename to client/src/pages/tutorialmanagement/TutorialManagement.tsx diff --git a/client/src/view/tutorialmanagement/components/TutorialTableRow.tsx b/client/src/pages/tutorialmanagement/components/TutorialTableRow.tsx similarity index 100% rename from client/src/view/tutorialmanagement/components/TutorialTableRow.tsx rename to client/src/pages/tutorialmanagement/components/TutorialTableRow.tsx diff --git a/client/src/view/usermanagement/UserManagement.tsx b/client/src/pages/usermanagement/UserManagement.tsx similarity index 100% rename from client/src/view/usermanagement/UserManagement.tsx rename to client/src/pages/usermanagement/UserManagement.tsx diff --git a/client/src/view/usermanagement/components/UserTableRow.tsx b/client/src/pages/usermanagement/components/UserTableRow.tsx similarity index 100% rename from client/src/view/usermanagement/components/UserTableRow.tsx rename to client/src/pages/usermanagement/components/UserTableRow.tsx diff --git a/client/src/routes/Routing.routes.ts b/client/src/routes/Routing.routes.ts index 289b06ac9..521173463 100644 --- a/client/src/routes/Routing.routes.ts +++ b/client/src/routes/Routing.routes.ts @@ -14,30 +14,30 @@ import { ViewDashboard as DashboardIcon, } from 'mdi-material-ui'; import { Role } from 'shared/model/Role'; -import AttendanceAdminView from '../view/attendance/AttendanceAdminView'; -import AttendanceView from '../view/attendance/AttendanceView'; -import CriteriaInfoView from '../view/criteria-info-view/CriteriaInfoView'; -import Dashboard from '../view/dashboard/Dashboard'; -import GenerateTutorials from '../view/generate-tutorials/GenerateTutorials'; -import ImportUsers from '../view/import-data/ImportUsers'; -import Login from '../view/Login'; -import EnterScheinexamPoints from '../view/points-scheinexam/enter-form/EnterScheinexamPoints'; -import ScheinexamPointsOverview from '../view/points-scheinexam/overview/ScheinexamPointsOverview'; -import EnterStudentPoints from '../view/points-sheet/enter-form/EnterStudentPoints'; -import EnterTeamPoints from '../view/points-sheet/enter-form/EnterTeamPoints'; -import PointsOverview from '../view/points-sheet/overview/PointsOverview'; -import PresentationPoints from '../view/presentation-points/PresentationPoints'; -import ScheinCriteriaManagement from '../view/scheincriteriamanagement/ScheinCriteriaManagement'; -import ScheinExamManagement from '../view/scheinexam-management/ScheinExamManagement'; -import SheetManagement from '../view/sheetmanagement/SheetManagement'; -import AllStudentsAdminView from '../view/studentmanagement/AllStudentsAdminView'; -import StudentInfo from '../view/studentmanagement/student-info/StudentInfo'; -import TutorStudentmanagement from '../view/studentmanagement/TutorStudentmanagement'; -import Teamoverview from '../view/teamoverview/Teamoverview'; -import TutorialInternalsManagement from '../view/tutorial-internals-management/TutorialInternalsManagement'; -import SubstituteManagement from '../view/tutorial-substitutes/SubstituteManagement'; -import TutorialManagement from '../view/tutorialmanagement/TutorialManagement'; -import UserManagement from '../view/usermanagement/UserManagement'; +import AttendanceAdminView from '../pages/attendance/AttendanceAdminView'; +import AttendanceView from '../pages/attendance/AttendanceView'; +import CriteriaInfoView from '../pages/criteria-info-view/CriteriaInfoView'; +import Dashboard from '../pages/dashboard/Dashboard'; +import GenerateTutorials from '../pages/generate-tutorials/GenerateTutorials'; +import ImportUsers from '../pages/import-data/ImportUsers'; +import Login from '../pages/Login'; +import EnterScheinexamPoints from '../pages/points-scheinexam/enter-form/EnterScheinexamPoints'; +import ScheinexamPointsOverview from '../pages/points-scheinexam/overview/ScheinexamPointsOverview'; +import EnterStudentPoints from '../pages/points-sheet/enter-form/EnterStudentPoints'; +import EnterTeamPoints from '../pages/points-sheet/enter-form/EnterTeamPoints'; +import PointsOverview from '../pages/points-sheet/overview/PointsOverview'; +import PresentationPoints from '../pages/presentation-points/PresentationPoints'; +import ScheinCriteriaManagement from '../pages/scheincriteriamanagement/ScheinCriteriaManagement'; +import ScheinExamManagement from '../pages/scheinexam-management/ScheinExamManagement'; +import SheetManagement from '../pages/sheetmanagement/SheetManagement'; +import StudentInfo from '../pages/student-info/StudentInfo'; +import AllStudentsAdminView from '../pages/studentmanagement/AllStudentsAdminView'; +import TutorStudentmanagement from '../pages/studentmanagement/TutorStudentmanagement'; +import Teamoverview from '../pages/teamoverview/Teamoverview'; +import TutorialInternalsManagement from '../pages/tutorial-internals-management/TutorialInternalsManagement'; +import SubstituteManagement from '../pages/tutorial-substitutes/SubstituteManagement'; +import TutorialManagement from '../pages/tutorialmanagement/TutorialManagement'; +import UserManagement from '../pages/usermanagement/UserManagement'; import { CustomRoute, DrawerRoute, parts, PrivateRoute } from './Routing.types'; import { param } from './typesafe-react-router'; diff --git a/client/src/test/App.test.tsx b/client/src/test/App.test.tsx index 629495e3d..df502aaa3 100644 --- a/client/src/test/App.test.tsx +++ b/client/src/test/App.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { MemoryRouter } from 'react-router'; import ContextWrapper from '../components/ContextWrapper'; -import App from '../view/App'; +import App from '../pages/App'; test('renders without crashing', () => { const rootDiv = document.createElement('div'); diff --git a/client/src/util/throwFunctions.ts b/client/src/util/throwFunctions.ts index 15f8d4ee3..67a3a09d9 100644 --- a/client/src/util/throwFunctions.ts +++ b/client/src/util/throwFunctions.ts @@ -4,7 +4,7 @@ * @param contextName Name of the context. * @returns Function that throws an `Error` (see above). */ -export function notInitializied(contextName: string) { +export function throwContextNotInitialized(contextName: string) { return (): never => { throw new Error(`Context '${contextName}' not initialised.`); }; From f05fcf719eb9a3fb26049511315b42aa14858691 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 11:59:05 +0200 Subject: [PATCH 21/62] Add helper function in settings context. This helper allows a unified check if the current user is allowed to excuse a student. --- client/src/hooks/useSettings.tsx | 26 +++++++++++++++++-- .../components/AttendanceInformation.tsx | 3 +++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/client/src/hooks/useSettings.tsx b/client/src/hooks/useSettings.tsx index 929428666..e55ba9a00 100644 --- a/client/src/hooks/useSettings.tsx +++ b/client/src/hooks/useSettings.tsx @@ -1,12 +1,16 @@ -import React, { useContext } from 'react'; +import React, { useCallback, useContext } from 'react'; +import { Role } from 'shared/model/Role'; import { IClientSettings } from 'shared/model/Settings'; import { RequireChildrenProp } from '../typings/RequireChildrenProp'; +import { throwContextNotInitialized } from '../util/throwFunctions'; import { getSettings } from './fetching/Settings'; +import { useLogin } from './LoginService'; import { useFetchState } from './useFetchState'; interface ContextType { settings: IClientSettings; isLoadingSettings: boolean; + canStudentBeExcused: () => boolean; } const DEFAULT_SETTINGS: IClientSettings = { defaultTeamSize: 1, canTutorExcuseStudents: false }; @@ -14,18 +18,36 @@ const DEFAULT_SETTINGS: IClientSettings = { defaultTeamSize: 1, canTutorExcuseSt const SettingsContext = React.createContext({ settings: DEFAULT_SETTINGS, isLoadingSettings: false, + canStudentBeExcused: throwContextNotInitialized('SettingsContext'), }); export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element { + const { userData } = useLogin(); const { value, isLoading } = useFetchState({ fetchFunction: getSettings, immediate: true, params: [], }); + const canStudentBeExcused = useCallback(() => { + if (!value) { + return false; + } + + if (value.canTutorExcuseStudents) { + return true; + } + + return !!userData && userData.roles.includes(Role.ADMIN); + }, [userData, value]); + return ( {children} diff --git a/client/src/pages/student-info/components/AttendanceInformation.tsx b/client/src/pages/student-info/components/AttendanceInformation.tsx index e0429e05e..3532b52e7 100644 --- a/client/src/pages/student-info/components/AttendanceInformation.tsx +++ b/client/src/pages/student-info/components/AttendanceInformation.tsx @@ -3,6 +3,7 @@ import { DateTime } from 'luxon'; import React from 'react'; import { AttendanceState, IAttendance } from 'shared/model/Attendance'; import AttendanceControls from '../../../components/attendance-controls/AttendanceControls'; +import { useSettings } from '../../../hooks/useSettings'; import { Student } from '../../../model/Student'; import { Tutorial } from '../../../model/Tutorial'; import { parseDateToMapKey } from '../../../util/helperFunctions'; @@ -21,6 +22,7 @@ function AttendanceInformation({ onNoteChange, ...props }: Props): JSX.Element { + const { canStudentBeExcused } = useSettings(); return (
@@ -44,6 +46,7 @@ function AttendanceInformation({ attendance={attendance} onAttendanceChange={(attendance) => onAttendanceChange(date, attendance)} onNoteChange={({ note }) => onNoteChange(date, note)} + excuseDisabled={!canStudentBeExcused()} justifyContent='flex-end' /> From 2a5b5657b118c6bc63ae4091d7b7ad7385af501c Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 12:26:52 +0200 Subject: [PATCH 22/62] Fix fetching settings to only occur AFTER login. Trying to load the settings before logging in results in a 401 error code and the settings were never re-fetched. --- client/src/hooks/useSettings.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/client/src/hooks/useSettings.tsx b/client/src/hooks/useSettings.tsx index e55ba9a00..49bc27c96 100644 --- a/client/src/hooks/useSettings.tsx +++ b/client/src/hooks/useSettings.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext } from 'react'; +import React, { useCallback, useContext, useEffect } from 'react'; import { Role } from 'shared/model/Role'; import { IClientSettings } from 'shared/model/Settings'; import { RequireChildrenProp } from '../typings/RequireChildrenProp'; @@ -23,11 +23,13 @@ const SettingsContext = React.createContext({ export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element { const { userData } = useLogin(); - const { value, isLoading } = useFetchState({ - fetchFunction: getSettings, - immediate: true, - params: [], - }); + const { value, isLoading, execute } = useFetchState({ fetchFunction: getSettings }); + + useEffect(() => { + if (!!userData) { + execute(); + } + }, [userData, execute]); const canStudentBeExcused = useCallback(() => { if (!value) { From f9a9d7cb9bc0db34a2809c21e5e7b8dc98df51c2 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 16:02:25 +0200 Subject: [PATCH 23/62] Clean up ununsed client components. --- .../pages/management/components/InfoTable.tsx | 62 ------ .../components/ScheinCriteriaStatusTable.tsx | 185 ------------------ .../management/components/StatusProgress.tsx | 57 ------ 3 files changed, 304 deletions(-) delete mode 100644 client/src/pages/management/components/InfoTable.tsx delete mode 100644 client/src/pages/management/components/ScheinCriteriaStatusTable.tsx delete mode 100644 client/src/pages/management/components/StatusProgress.tsx diff --git a/client/src/pages/management/components/InfoTable.tsx b/client/src/pages/management/components/InfoTable.tsx deleted file mode 100644 index 20db462e9..000000000 --- a/client/src/pages/management/components/InfoTable.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; -import { Typography, Table, TableBody, TableRow } from '@material-ui/core'; -import { TableProps } from '@material-ui/core/Table'; -import clsx from 'clsx'; - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - tableWithoutBorders: { - '& td': { - borderBottom: 'none', - }, - }, - spacingRow: { - height: theme.spacing(1.5), - }, - placeholder: { - marginTop: theme.spacing(2), - textAlign: 'center', - }, - }) -); - -export interface InfoTableProps extends TableProps { - items: T[]; - createRowFromItem: (item: T) => React.ReactNode; - placeholder?: string; -} - -function InfoTable({ - items, - createRowFromItem, - placeholder, - className, - ...other -}: InfoTableProps): JSX.Element { - const classes = useStyles(); - - return ( - <> - {items.length === 0 ? ( - - {placeholder} - - ) : ( -
- - {items.map((item, idx) => ( - - - - {createRowFromItem(item)} - - ))} - -
- )} - - ); -} - -export default InfoTable; diff --git a/client/src/pages/management/components/ScheinCriteriaStatusTable.tsx b/client/src/pages/management/components/ScheinCriteriaStatusTable.tsx deleted file mode 100644 index 2c8fb5b0f..000000000 --- a/client/src/pages/management/components/ScheinCriteriaStatusTable.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { - createStyles, - makeStyles, - Paper, - Table, - TableBody, - TableCell, - TableRow, - Theme, - Tooltip, -} from '@material-ui/core'; -import clsx from 'clsx'; -import React, { useState } from 'react'; -import { ScheinCriteriaStatus } from 'shared/model/ScheinCriteria'; -import { useTranslation } from '../../../util/lang/configI18N'; -import InfoTable from './InfoTable'; -import StatusProgress from './StatusProgress'; - -interface Props { - summary: ScheinCriteriaStatus[]; -} - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - width: '100%', - paddingBottom: theme.spacing(1.5), - borderTopLeftRadius: 0, - borderTopRightRadius: 0, - boxShadow: - 'inset 0px 2px 3px -2px rgba(0,0,0,0.8), 0px 1px 3px 0px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 2px 1px -1px rgba(0,0,0,0.12)', - }, - infoRowCell: { - paddingTop: theme.spacing(0.5), - }, - infoRowContent: { - maxWidth: '95%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginLeft: 'auto', - marginRight: 'auto', - }, - infoRowTable: { - width: 'unset', - }, - placeholder: { - marginTop: 64, - textAlign: 'center', - }, - progress: { - maxWidth: '300px', - minWidth: '200px', - margin: theme.spacing(1), - }, - statusInfosRowWithDetail: { - '&:hover': { - cursor: 'pointer', - boxShadow: theme.shadows[4], - }, - }, - statusInfosRowCell: { - padding: theme.spacing(0.5), - textAlign: 'center', - '&:not(:last-child)': { - borderRight: '1px solid black', - }, - '&:last-child': { - paddingRight: theme.spacing(0.5), - }, - }, - bottomInfosRowCell: { - borderTop: '1px solid black', - }, - }) -); - -interface InfoContentProps { - status: ScheinCriteriaStatus; -} - -function InfosContent({ status }: InfoContentProps): JSX.Element | null { - const classes = useStyles(); - const { t } = useTranslation('scheincriteria'); - - const infos = Object.values(status.infos); - - if (infos.length === 0) { - return null; - } - - return ( - - - - {infos - .sort((a, b) => a.no - b.no) - .map((item, idx) => ( - - {item.no} - - ))} - - {t(`UNIT_LABEL_${status.unit}`)} - - - - {infos - .sort((a, b) => a.no - b.no) - .map((item, idx) => ( - - - {item.achieved} / {item.total} - - - ))} - - {t(`UNIT_LABEL_${infos[0].unit}_plural`)} - - - -
- ); -} - -function ScheinCriteriaStatusTable({ summary }: Props): JSX.Element { - const classes = useStyles(); - const { t } = useTranslation('scheincriteria'); - - const [showDetailedInfos, setShowDetailedInfos] = useState(false); - - return ( - - ( - - {status.name.length > 0 ? ( - {status.name} - ) : ( - {t(status.identifier)} - )} - - - - {Object.values(status.infos).length > 0 && showDetailedInfos ? ( - 0 ? classes.statusInfosRowWithDetail : '' - } - onClick={() => { - setShowDetailedInfos(!showDetailedInfos); - }} - > - - - ) : Object.values(status.infos).length === 0 ? ( - - {status.achieved + '/' + status.total + ' ' + t(`UNIT_LABEL_${status.unit}_plural`)} - - ) : ( - - 0 ? classes.statusInfosRowWithDetail : '' - } - onClick={() => { - setShowDetailedInfos(!showDetailedInfos); - }} - > - {`${status.achieved}/${status.total} ${t(`UNIT_LABEL_${status.unit}_plural`)}`} - - - )} - - )} - /> - - ); -} - -export default ScheinCriteriaStatusTable; diff --git a/client/src/pages/management/components/StatusProgress.tsx b/client/src/pages/management/components/StatusProgress.tsx deleted file mode 100644 index 9523d3e38..000000000 --- a/client/src/pages/management/components/StatusProgress.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { LinearProgress, withStyles } from '@material-ui/core'; -import GREEN from '@material-ui/core/colors/green'; -import RED from '@material-ui/core/colors/red'; -import { LinearProgressProps } from '@material-ui/core/LinearProgress'; -import { lighten } from '@material-ui/core/styles'; -import React from 'react'; - -interface StatusProgressState { - achieved: number; - total: number; - passed: boolean; -} - -interface Props extends Omit { - status?: StatusProgressState; -} - -const BorderLinearProgress = withStyles({ - root: { - height: 10, - borderRadius: 4, - }, - bar: { - borderRadius: 4, - }, - colorPrimary: { - backgroundColor: lighten(GREEN[600], 0.5), - }, - barColorPrimary: { - backgroundColor: GREEN[600], - }, - colorSecondary: { - backgroundColor: lighten(RED[600], 0.5), - }, - barColorSecondary: { - backgroundColor: RED[600], - }, -})(LinearProgress); - -function StatusProgress({ status, ...other }: Props): JSX.Element { - if (!status) { - return ; - } - - const achieved = status.achieved < status.total ? status.achieved : status.total; - - return ( - - ); -} - -export default StatusProgress; From 9851bd324d8b0a22953db19be5200159ececd563 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 16:02:36 +0200 Subject: [PATCH 24/62] Add linter rule for ununsed module exports. --- .eslintrc.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc.yml b/.eslintrc.yml index 0e2528831..feae93a6c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -56,6 +56,9 @@ rules: from: './server/src' 'import/no-unresolved': 'off' 'import/no-named-as-default': 'off' + 'import/no-unused-modules': + - 'warn' + - unusedExports: true 'react/display-name': 'off' 'react/jsx-no-duplicate-props': - 'warn' From ae1e188b305e8b728d254b12c11ae803c4951467 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 16:03:34 +0200 Subject: [PATCH 25/62] Add general layout for settings page. --- client/src/pages/settings/SettingsPage.tsx | 120 +++++++++++++++++++++ client/src/routes/Routing.routes.ts | 12 ++- 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/settings/SettingsPage.tsx diff --git a/client/src/pages/settings/SettingsPage.tsx b/client/src/pages/settings/SettingsPage.tsx new file mode 100644 index 000000000..23db6501e --- /dev/null +++ b/client/src/pages/settings/SettingsPage.tsx @@ -0,0 +1,120 @@ +import { Box, Divider, Typography } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import { Formik } from 'formik'; +import React, { useCallback, useMemo } from 'react'; +import * as Yup from 'yup'; +import FormikCheckbox from '../../components/forms/components/FormikCheckbox'; +import FormikTextField from '../../components/forms/components/FormikTextField'; +import SubmitButton from '../../components/loading/SubmitButton'; +import Placeholder from '../../components/Placeholder'; +import { useSettings } from '../../hooks/useSettings'; +import { FormikSubmitCallback } from '../../types'; + +const useStyles = makeStyles((theme) => + createStyles({ + form: { display: 'flex', flexDirection: 'column' }, + unsavedChangesLabel: { marginLeft: theme.spacing(1) }, + input: { margin: theme.spacing(1, 0) }, + }) +); + +const validationSchema = Yup.object().shape({ + canTutorExcuseStudents: Yup.boolean().required('Benötigt'), + defaultTeamSize: Yup.number() + .integer('Muss eine ganze Zahl sein.') + .min(1, 'Muss mindestens 1 sein.') + .required('Benötigt'), +}); + +interface FormState { + defaultTeamSize: string; + canTutorExcuseStudents: boolean; +} + +function GridDivider(): JSX.Element { + return ; +} + +function SettingsPage(): JSX.Element { + const classes = useStyles(); + const { isLoadingSettings, settings } = useSettings(); + + const initialValues: FormState = useMemo(() => { + return { + canTutorExcuseStudents: settings.canTutorExcuseStudents, + defaultTeamSize: `${settings.defaultTeamSize}`, + }; + }, [settings]); + const onSubmit: FormikSubmitCallback = useCallback(async (values, helpers) => { + return new Promise((resolve) => { + setTimeout(() => resolve(), 5000); + }); + }, []); + + return ( + + + {({ handleSubmit, isValid, dirty, isSubmitting }) => ( +
+ + + Einstellungen speichern + + + {dirty && ( + + Es gibt ungespeicherte Änderungen. + + )} + + + + Standardteamgröße + + + + + Anwesenheiten + + + + + E-Maileinstellungen + 🛠 Work in progress + +
+ )} +
+
+ ); +} + +export default SettingsPage; diff --git a/client/src/routes/Routing.routes.ts b/client/src/routes/Routing.routes.ts index 521173463..5ac4654f5 100644 --- a/client/src/routes/Routing.routes.ts +++ b/client/src/routes/Routing.routes.ts @@ -5,6 +5,7 @@ import { AccountMultipleCheck as AttendancesIcon, BadgeAccount as UserIcon, Book as EnterPointsIcon, + Cogs as SettingsIcon, Comment as PresentationIcon, File as SheetIcon, ScriptText as ScheincriteriaIcon, @@ -29,6 +30,7 @@ import PointsOverview from '../pages/points-sheet/overview/PointsOverview'; import PresentationPoints from '../pages/presentation-points/PresentationPoints'; import ScheinCriteriaManagement from '../pages/scheincriteriamanagement/ScheinCriteriaManagement'; import ScheinExamManagement from '../pages/scheinexam-management/ScheinExamManagement'; +import SettingsPage from '../pages/settings/SettingsPage'; import SheetManagement from '../pages/sheetmanagement/SheetManagement'; import StudentInfo from '../pages/student-info/StudentInfo'; import AllStudentsAdminView from '../pages/studentmanagement/AllStudentsAdminView'; @@ -260,6 +262,13 @@ const MANAGEMENT_ROUTES = { icon: ScheinexamManagementIcon, roles: [Role.ADMIN, Role.EMPLOYEE], }), + MANAGE_SETTINGS: new DrawerRoute({ + path: parts('admin', 'settings'), + title: 'Einstellungen anpassen', + component: SettingsPage, + icon: SettingsIcon, + roles: [Role.ADMIN], + }), }; export const ROUTES = { @@ -270,6 +279,3 @@ export const ROUTES = { export const ROOT_REDIRECT_PATH = ROUTES.LOGIN; export const PATH_REDIRECT_AFTER_LOGIN = ROUTES.DASHBOARD; - -// type RouteKeys = keyof typeof ROUTES; -// export type RouteType = typeof ROUTES[RouteKeys]; From df41c4e0bdc9b39d5c13fd43fe10205452f7b419 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 16:04:02 +0200 Subject: [PATCH 26/62] Change style to be more consistent between light & dark theme. This is the first step in an ongoing effort. --- client/src/util/styles.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/client/src/util/styles.ts b/client/src/util/styles.ts index aa1c7a07e..b7144cd75 100644 --- a/client/src/util/styles.ts +++ b/client/src/util/styles.ts @@ -1,6 +1,6 @@ import { createMuiTheme, PaletteType, Theme } from '@material-ui/core'; import ORANGE from '@material-ui/core/colors/orange'; -import { PaletteOptions } from '@material-ui/core/styles/createPalette'; +import { PaletteOptions, SimplePaletteColorOptions } from '@material-ui/core/styles/createPalette'; import { CSSProperties } from '@material-ui/styles'; interface ChartStyle { @@ -83,11 +83,15 @@ function generateChartStyle(theme: Theme): ChartStyle { } export function createTheme(type: PaletteType): Theme { + const primary: SimplePaletteColorOptions = { + light: '#00beff', + main: '#004191', + dark: '#001b63', + // main: type === 'light' ? '#004191' : '#00a5fe', + }; const palette: PaletteOptions = { type, - primary: { - main: type === 'light' ? '#004191' : '#00a5fe', - }, + primary, secondary: { main: '#00beff', }, @@ -110,6 +114,8 @@ export function createTheme(type: PaletteType): Theme { }, }; + const focusedColor = type === 'light' ? primary.main : primary.light; + return createMuiTheme({ palette, mixins: { @@ -119,6 +125,13 @@ export function createTheme(type: PaletteType): Theme { chart: generateChartStyle, }, overrides: { + MuiFormLabel: { + root: { + '&$focused': { + color: focusedColor, + }, + }, + }, MuiOutlinedInput: { root: { '&$disabled': { @@ -128,6 +141,13 @@ export function createTheme(type: PaletteType): Theme { }, }, }, + '&$focused': { + '&$focused': { + '& > fieldset': { + borderColor: focusedColor, + }, + }, + }, }, }, }, From ce6e215bd23602442cc28addf08c56138eb9aee3 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 17:34:50 +0200 Subject: [PATCH 27/62] Add helper function to theme palette. This helps to fix some theme specific issues (ie primary.main being to dark for certain applications in the dark theme). --- client/src/components/loading/LoadingSpinner.tsx | 9 +++++---- client/src/components/loading/SubmitButton.tsx | 1 + client/src/util/styles.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/src/components/loading/LoadingSpinner.tsx b/client/src/components/loading/LoadingSpinner.tsx index 81f3b312a..7ff6fa9a6 100644 --- a/client/src/components/loading/LoadingSpinner.tsx +++ b/client/src/components/loading/LoadingSpinner.tsx @@ -1,6 +1,6 @@ +import { CircularProgress, Theme, Typography } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import { makeStyles, createStyles } from '@material-ui/core/styles'; -import { CircularProgress, Typography, Theme } from '@material-ui/core'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -11,6 +11,7 @@ const useStyles = makeStyles((theme: Theme) => height: '100%', alignItems: 'center', justifyContent: 'center', + color: theme.palette.getThemeContrastColor(theme.palette.primary), }, spinner: { marginBottom: theme.spacing(3), @@ -23,9 +24,9 @@ function LoadingSpinner(): JSX.Element { return (
- + - + Lade Daten...
diff --git a/client/src/components/loading/SubmitButton.tsx b/client/src/components/loading/SubmitButton.tsx index 53ab88d1e..3b1b00737 100644 --- a/client/src/components/loading/SubmitButton.tsx +++ b/client/src/components/loading/SubmitButton.tsx @@ -10,6 +10,7 @@ const useStyles = makeStyles((theme: Theme) => createStyles({ spinner: { marginRight: theme.spacing(1), + color: theme.palette.getThemeContrastColor(theme.palette.primary), }, }) ); diff --git a/client/src/util/styles.ts b/client/src/util/styles.ts index b7144cd75..65a76c0ad 100644 --- a/client/src/util/styles.ts +++ b/client/src/util/styles.ts @@ -23,12 +23,18 @@ declare module '@material-ui/core/styles/createPalette' { green: Omit; orange: Omit; red: Omit; + + /** + * @returns Color variant depending on theme type: If on a light theme the `main` part is returned else if on a dark theme the `light` part is returned (falling back to `main` if `light` does not exist). + */ + getThemeContrastColor: (palette: SimplePaletteColorOptions) => string; } interface PaletteOptions { green: Palette['green']; orange: Palette['orange']; red: Palette['red']; + getThemeContrastColor: (palette: SimplePaletteColorOptions) => string; } } @@ -112,6 +118,9 @@ export function createTheme(type: PaletteType): Theme { main: ORANGE[500], dark: ORANGE[700], }, + getThemeContrastColor: (palette: SimplePaletteColorOptions) => { + return type === 'light' ? palette['main'] : palette['light'] ?? palette['main']; + }, }; const focusedColor = type === 'light' ? primary.main : primary.light; From a5ff6a9fed83793e9378b9d342c7aa39303ea930 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 17:36:19 +0200 Subject: [PATCH 28/62] Add logic to save new settings in the client. This also adds the possibility to update the settings by a helper functions returned by useSettings. --- client/src/hooks/fetching/Settings.ts | 10 +++++++++- client/src/hooks/useSettings.tsx | 12 ++++++++--- client/src/pages/settings/SettingsPage.tsx | 23 ++++++++++++++++------ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/client/src/hooks/fetching/Settings.ts b/client/src/hooks/fetching/Settings.ts index ee8909671..53cf79c98 100644 --- a/client/src/hooks/fetching/Settings.ts +++ b/client/src/hooks/fetching/Settings.ts @@ -2,7 +2,7 @@ import { IClientSettings } from 'shared/model/Settings'; import axios from './Axios'; export async function getSettings(): Promise { - const response = await axios.get(`setting`); + const response = await axios.get('setting'); if (response.status === 200) { return response.data; @@ -10,3 +10,11 @@ export async function getSettings(): Promise { return Promise.reject(`Wrong status code (${response.status}).`); } + +export async function setSettings(dto: IClientSettings): Promise { + const response = await axios.put('setting', dto); + + if (response.status !== 200) { + return Promise.reject(`Wrong status code (${response.status}).`); + } +} diff --git a/client/src/hooks/useSettings.tsx b/client/src/hooks/useSettings.tsx index 49bc27c96..e45199504 100644 --- a/client/src/hooks/useSettings.tsx +++ b/client/src/hooks/useSettings.tsx @@ -11,6 +11,7 @@ interface ContextType { settings: IClientSettings; isLoadingSettings: boolean; canStudentBeExcused: () => boolean; + updateSettings: () => Promise; } const DEFAULT_SETTINGS: IClientSettings = { defaultTeamSize: 1, canTutorExcuseStudents: false }; @@ -19,18 +20,22 @@ const SettingsContext = React.createContext({ settings: DEFAULT_SETTINGS, isLoadingSettings: false, canStudentBeExcused: throwContextNotInitialized('SettingsContext'), + updateSettings: throwContextNotInitialized('SettingsContext'), }); export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element { const { userData } = useLogin(); const { value, isLoading, execute } = useFetchState({ fetchFunction: getSettings }); - - useEffect(() => { + const updateSettings = useCallback(async () => { if (!!userData) { - execute(); + await execute(); } }, [userData, execute]); + useEffect(() => { + updateSettings(); + }, [updateSettings]); + const canStudentBeExcused = useCallback(() => { if (!value) { return false; @@ -49,6 +54,7 @@ export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element settings: value ?? DEFAULT_SETTINGS, isLoadingSettings: isLoading, canStudentBeExcused, + updateSettings, }} > {children} diff --git a/client/src/pages/settings/SettingsPage.tsx b/client/src/pages/settings/SettingsPage.tsx index 23db6501e..e9da0e188 100644 --- a/client/src/pages/settings/SettingsPage.tsx +++ b/client/src/pages/settings/SettingsPage.tsx @@ -2,11 +2,13 @@ import { Box, Divider, Typography } from '@material-ui/core'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import { Formik } from 'formik'; import React, { useCallback, useMemo } from 'react'; +import { IClientSettings } from 'shared/model/Settings'; import * as Yup from 'yup'; import FormikCheckbox from '../../components/forms/components/FormikCheckbox'; import FormikTextField from '../../components/forms/components/FormikTextField'; import SubmitButton from '../../components/loading/SubmitButton'; import Placeholder from '../../components/Placeholder'; +import { setSettings } from '../../hooks/fetching/Settings'; import { useSettings } from '../../hooks/useSettings'; import { FormikSubmitCallback } from '../../types'; @@ -37,7 +39,7 @@ function GridDivider(): JSX.Element { function SettingsPage(): JSX.Element { const classes = useStyles(); - const { isLoadingSettings, settings } = useSettings(); + const { isLoadingSettings, settings, updateSettings } = useSettings(); const initialValues: FormState = useMemo(() => { return { @@ -45,11 +47,20 @@ function SettingsPage(): JSX.Element { defaultTeamSize: `${settings.defaultTeamSize}`, }; }, [settings]); - const onSubmit: FormikSubmitCallback = useCallback(async (values, helpers) => { - return new Promise((resolve) => { - setTimeout(() => resolve(), 5000); - }); - }, []); + const onSubmit: FormikSubmitCallback = useCallback( + async (values, helpers) => { + const dto: IClientSettings = { + canTutorExcuseStudents: values.canTutorExcuseStudents, + defaultTeamSize: Number.parseInt(values.defaultTeamSize), + }; + + await setSettings(dto); + await updateSettings(); + + // helpers.resetForm({ values }); + }, + [updateSettings] + ); return ( Date: Wed, 29 Jul 2020 18:21:04 +0200 Subject: [PATCH 29/62] Add 'no-useless-rename' rule to eslint config. --- .eslintrc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index feae93a6c..0cd65c2bf 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -49,6 +49,7 @@ rules: 'constructor-super': 'error' 'no-console': 'warn' 'no-fallthrough': 'error' + 'no-useless-rename': 'warn' 'import/no-restricted-paths': - 'error' - zones: @@ -57,7 +58,7 @@ rules: 'import/no-unresolved': 'off' 'import/no-named-as-default': 'off' 'import/no-unused-modules': - - 'warn' + - 'off' - unusedExports: true 'react/display-name': 'off' 'react/jsx-no-duplicate-props': From c04c318291f6d469824bf3e21ebc2ebc8d846caf Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 18:21:22 +0200 Subject: [PATCH 30/62] Add new promise wrapper that can show notifications on success/error. --- .../src/hooks/snackbar/useCustomSnackbar.ts | 14 +++- .../src/hooks/snackbar/usePromiseSnackbar.ts | 69 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 client/src/hooks/snackbar/usePromiseSnackbar.ts diff --git a/client/src/hooks/snackbar/useCustomSnackbar.ts b/client/src/hooks/snackbar/useCustomSnackbar.ts index 2f704b159..a39c9e76c 100644 --- a/client/src/hooks/snackbar/useCustomSnackbar.ts +++ b/client/src/hooks/snackbar/useCustomSnackbar.ts @@ -4,11 +4,21 @@ import { UseSnackbarWithList, } from '../../components/snackbar-with-list/useSnackbarWithList'; import { useErrorSnackbar, UseErrorSnackbar } from './useErrorSnackbar'; +import { usePromiseSnackbar, UsePromiseSnackbar } from './usePromiseSnackbar'; -export function useCustomSnackbar(): ProviderContext & UseErrorSnackbar & UseSnackbarWithList { +export function useCustomSnackbar(): ProviderContext & + UseErrorSnackbar & + UseSnackbarWithList & + UsePromiseSnackbar { const useSnackbarFunctions = useSnackbar(); const useErrorSnackbarFunctions = useErrorSnackbar(); const useSnackbarWithListFunctions = useSnackbarWithList(); + const usePromiseSnackbarFunctions = usePromiseSnackbar(); - return { ...useSnackbarFunctions, ...useErrorSnackbarFunctions, ...useSnackbarWithListFunctions }; + return { + ...useSnackbarFunctions, + ...useErrorSnackbarFunctions, + ...useSnackbarWithListFunctions, + ...usePromiseSnackbarFunctions, + }; } diff --git a/client/src/hooks/snackbar/usePromiseSnackbar.ts b/client/src/hooks/snackbar/usePromiseSnackbar.ts new file mode 100644 index 000000000..b9384484e --- /dev/null +++ b/client/src/hooks/snackbar/usePromiseSnackbar.ts @@ -0,0 +1,69 @@ +import { useSnackbar } from 'notistack'; +import { ReactNode, useCallback } from 'react'; + +type BaseArrayType = readonly unknown[]; + +interface RunParams

{ + /** Function that gets wrapped */ + promiseFunction: (...args: P) => Promise; + + /** If provided, on a success a success snackbar with this as content will be shown. */ + successContent?: ReactNode; + + /** If provided, on an error an error snackbar with this as content will be shown. */ + errorContent?: ReactNode; + + /** If provided it will get called with the thrown error if the `promiseFunction` throws one. If not provided the error gets swalloed. */ + errorHandler?: (error: unknown) => void; +} + +type RunFunction =

( + params: RunParams +) => (...args: P) => Promise; + +export interface UsePromiseSnackbar { + /** + * Creates and returns a wrapping function for the given `promiseFunction`. + * + * The created function returns whatever `promiseFunction` would return if successfully finished and it takes exactly the parameters which the `promiseFunction` would take. + * + * On completion of that function and if a `success` message is provided a success snackbar is shown. On error and if an `error` message is provided an error snackbar is shown. + * + * Please note that if no `errorHandler` is provided the wrapping function will simply ignore any thrown errors and will swallow them. + * + * @returns Async function which calls the given `promiseFunction`. + */ + promiseWithSnackbar: RunFunction; +} + +export function usePromiseSnackbar(): UsePromiseSnackbar { + const { enqueueSnackbar } = useSnackbar(); + const promiseWithSnackbar: RunFunction = useCallback( + (params: RunParams) => { + return async (...args: BaseArrayType) => { + const { promiseFunction, successContent, errorContent, errorHandler } = params; + + try { + const result = await promiseFunction(...args); + + if (!!successContent) { + enqueueSnackbar(successContent, { variant: 'success' }); + } + + return result; + } catch (err) { + if (!!errorContent) { + enqueueSnackbar(errorContent, { variant: 'error' }); + } + + if (!!errorHandler) { + errorHandler(err); + } + } + }; + }, + [enqueueSnackbar] + ); + + return { promiseWithSnackbar }; +} From 7091c01d31b5368472ba63733bd325ae965a1b91 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 18:21:38 +0200 Subject: [PATCH 31/62] Add notifications to the submit process of the SettingsPage. --- client/src/pages/settings/SettingsPage.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/src/pages/settings/SettingsPage.tsx b/client/src/pages/settings/SettingsPage.tsx index e9da0e188..0986475fc 100644 --- a/client/src/pages/settings/SettingsPage.tsx +++ b/client/src/pages/settings/SettingsPage.tsx @@ -9,6 +9,7 @@ import FormikTextField from '../../components/forms/components/FormikTextField'; import SubmitButton from '../../components/loading/SubmitButton'; import Placeholder from '../../components/Placeholder'; import { setSettings } from '../../hooks/fetching/Settings'; +import { useCustomSnackbar } from '../../hooks/snackbar/useCustomSnackbar'; import { useSettings } from '../../hooks/useSettings'; import { FormikSubmitCallback } from '../../types'; @@ -40,6 +41,7 @@ function GridDivider(): JSX.Element { function SettingsPage(): JSX.Element { const classes = useStyles(); const { isLoadingSettings, settings, updateSettings } = useSettings(); + const { promiseWithSnackbar } = useCustomSnackbar(); const initialValues: FormState = useMemo(() => { return { @@ -48,18 +50,23 @@ function SettingsPage(): JSX.Element { }; }, [settings]); const onSubmit: FormikSubmitCallback = useCallback( - async (values, helpers) => { + async (values) => { const dto: IClientSettings = { canTutorExcuseStudents: values.canTutorExcuseStudents, defaultTeamSize: Number.parseInt(values.defaultTeamSize), }; - await setSettings(dto); - await updateSettings(); - - // helpers.resetForm({ values }); + await promiseWithSnackbar({ + promiseFunction: setSettings, + successContent: 'Einstellungen erfolgreich gespeichert.', + errorContent: 'Einstellungen konnten nicht gespeichert werden.', + })(dto); + await promiseWithSnackbar({ + promiseFunction: updateSettings, + errorContent: 'Neue Einstellungen konnten nicht abgerufen werden.', + })(); }, - [updateSettings] + [updateSettings, promiseWithSnackbar] ); return ( From 55b4009c31fddf602ba7cff2eeac342d668f030f Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 18:24:04 +0200 Subject: [PATCH 32/62] Change snackbars to be displayed on the bottom right. Instead of the bottom left which blocked user interactions to the drawer while notifcations were displayed. --- client/src/components/ContextWrapper.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/components/ContextWrapper.tsx b/client/src/components/ContextWrapper.tsx index acc945c54..70a9d6c21 100644 --- a/client/src/components/ContextWrapper.tsx +++ b/client/src/components/ContextWrapper.tsx @@ -85,7 +85,10 @@ function ContextWrapper({ children, Router }: PropsWithChildren): JSX.Ele - + {children} From c87802bc00d25a942fa531d0c30afbac56fe86ed Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 20:52:04 +0200 Subject: [PATCH 33/62] Revert style changes partially. The main primary color needs to be a light color in the dark theme. --- .../src/components/loading/LoadingSpinner.tsx | 5 ++-- .../src/components/loading/SubmitButton.tsx | 1 - client/src/util/styles.ts | 30 ++----------------- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/client/src/components/loading/LoadingSpinner.tsx b/client/src/components/loading/LoadingSpinner.tsx index 7ff6fa9a6..c0452716d 100644 --- a/client/src/components/loading/LoadingSpinner.tsx +++ b/client/src/components/loading/LoadingSpinner.tsx @@ -11,7 +11,6 @@ const useStyles = makeStyles((theme: Theme) => height: '100%', alignItems: 'center', justifyContent: 'center', - color: theme.palette.getThemeContrastColor(theme.palette.primary), }, spinner: { marginBottom: theme.spacing(3), @@ -24,9 +23,9 @@ function LoadingSpinner(): JSX.Element { return (

- + - + Lade Daten...
diff --git a/client/src/components/loading/SubmitButton.tsx b/client/src/components/loading/SubmitButton.tsx index 3b1b00737..53ab88d1e 100644 --- a/client/src/components/loading/SubmitButton.tsx +++ b/client/src/components/loading/SubmitButton.tsx @@ -10,7 +10,6 @@ const useStyles = makeStyles((theme: Theme) => createStyles({ spinner: { marginRight: theme.spacing(1), - color: theme.palette.getThemeContrastColor(theme.palette.primary), }, }) ); diff --git a/client/src/util/styles.ts b/client/src/util/styles.ts index 65a76c0ad..6de466554 100644 --- a/client/src/util/styles.ts +++ b/client/src/util/styles.ts @@ -23,18 +23,12 @@ declare module '@material-ui/core/styles/createPalette' { green: Omit; orange: Omit; red: Omit; - - /** - * @returns Color variant depending on theme type: If on a light theme the `main` part is returned else if on a dark theme the `light` part is returned (falling back to `main` if `light` does not exist). - */ - getThemeContrastColor: (palette: SimplePaletteColorOptions) => string; } interface PaletteOptions { green: Palette['green']; orange: Palette['orange']; red: Palette['red']; - getThemeContrastColor: (palette: SimplePaletteColorOptions) => string; } } @@ -91,9 +85,8 @@ function generateChartStyle(theme: Theme): ChartStyle { export function createTheme(type: PaletteType): Theme { const primary: SimplePaletteColorOptions = { light: '#00beff', - main: '#004191', - dark: '#001b63', - // main: type === 'light' ? '#004191' : '#00a5fe', + main: type === 'light' ? '#004191' : '#00a5fe', + dark: '#004191', }; const palette: PaletteOptions = { type, @@ -118,13 +111,8 @@ export function createTheme(type: PaletteType): Theme { main: ORANGE[500], dark: ORANGE[700], }, - getThemeContrastColor: (palette: SimplePaletteColorOptions) => { - return type === 'light' ? palette['main'] : palette['light'] ?? palette['main']; - }, }; - const focusedColor = type === 'light' ? primary.main : primary.light; - return createMuiTheme({ palette, mixins: { @@ -134,13 +122,6 @@ export function createTheme(type: PaletteType): Theme { chart: generateChartStyle, }, overrides: { - MuiFormLabel: { - root: { - '&$focused': { - color: focusedColor, - }, - }, - }, MuiOutlinedInput: { root: { '&$disabled': { @@ -150,13 +131,6 @@ export function createTheme(type: PaletteType): Theme { }, }, }, - '&$focused': { - '&$focused': { - '& > fieldset': { - borderColor: focusedColor, - }, - }, - }, }, }, }, From a2b5e579cda3a29491ba9db074a7166952148a4b Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 20:52:14 +0200 Subject: [PATCH 34/62] Change AppBar to always have the dark blue background color. This lets the bar appaer consistent between the two themes. --- client/src/pages/AppBar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/pages/AppBar.tsx b/client/src/pages/AppBar.tsx index 5bb56a69d..8e90251c4 100644 --- a/client/src/pages/AppBar.tsx +++ b/client/src/pages/AppBar.tsx @@ -34,6 +34,8 @@ import { saveBlob } from '../util/helperFunctions'; const useStyles = makeStyles((theme: Theme) => createStyles({ appBar: { + backgroundColor: theme.palette.primary.dark, + color: theme.palette.getContrastText(theme.palette.primary.dark), zIndex: theme.zIndex.drawer + 1, }, grow: { From a785fd986aaa2cf345b9aff8df70416bcdbe6d4b Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 20:52:20 +0200 Subject: [PATCH 35/62] Fix version clipping over NavigationRail items. --- client/src/components/navigation-rail/NavigationRail.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/src/components/navigation-rail/NavigationRail.tsx b/client/src/components/navigation-rail/NavigationRail.tsx index 726d6fee4..e687007c8 100644 --- a/client/src/components/navigation-rail/NavigationRail.tsx +++ b/client/src/components/navigation-rail/NavigationRail.tsx @@ -47,17 +47,14 @@ const useStyles = makeStyles((theme) => }, toolbar: theme.mixins.toolbar, list: { - paddingBottom: theme.spacing(4), overflowY: 'auto', overflowX: 'hidden', ...theme.mixins.scrollbar(4), }, version: { - position: 'absolute', - bottom: theme.spacing(1), - left: theme.spacing(1), - right: theme.spacing(1), + margin: theme.spacing(0.5, 0), textAlign: 'center', + width: '100%', }, }) ); From 17691781a5daa066ac687513eb1425093bdaf050 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 20:53:08 +0200 Subject: [PATCH 36/62] Change setting titles on the left to be 1.1rem in font-size. This replaces the usage of 'h6' components. --- client/src/pages/settings/SettingsPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/pages/settings/SettingsPage.tsx b/client/src/pages/settings/SettingsPage.tsx index 0986475fc..82eb9e567 100644 --- a/client/src/pages/settings/SettingsPage.tsx +++ b/client/src/pages/settings/SettingsPage.tsx @@ -107,7 +107,7 @@ function SettingsPage(): JSX.Element { maxHeight='100%' style={{ overflowY: 'auto' }} > - Standardteamgröße + Standardteamgröße - Anwesenheiten + Anwesenheiten - E-Maileinstellungen + E-Maileinstellungen 🛠 Work in progress From 2094a6661b39d918d8cba933f1ab8ff2b3e0d7d7 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 23:40:54 +0200 Subject: [PATCH 37/62] Add es2020 TS-lib to allow Promise.allSettled. --- server/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/tsconfig.json b/server/tsconfig.json index b9d1b7fd4..c0e13d740 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -14,7 +14,8 @@ }, "strict": true, "incremental": true, - "esModuleInterop": true + "esModuleInterop": true, + "lib": ["es2020"] }, "exclude": ["node_modules", "dist"] } From 4aed016303286c30aaac4f7384e10c2289f4c685 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 23:42:06 +0200 Subject: [PATCH 38/62] Move mailing configuration from static settings to SettingsModel. --- server/config/development.yml | 8 -------- server/src/database/models/settings.model.ts | 5 +++++ .../settings/model/ApplicationConfiguration.ts | 5 ----- .../module/settings/model/MailingConfiguration.ts | 13 +++++++------ server/src/module/settings/settings.service.ts | 9 +++++++++ server/src/module/settings/settings.static.ts | 8 -------- server/src/shared/model/Mail.ts | 1 + server/src/shared/model/Settings.ts | 13 +++++++++++++ 8 files changed, 35 insertions(+), 27 deletions(-) diff --git a/server/config/development.yml b/server/config/development.yml index 8319696be..3dd32fea2 100644 --- a/server/config/development.yml +++ b/server/config/development.yml @@ -15,11 +15,3 @@ database: defaultSettings: canTutorExcuseStudents: true defaultTeamSize: 3 - -mailing: - testingMode: true - host: HOST - port: 587 - auth: - user: USER - pass: PASS diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index fec43a7c5..e6b18c722 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -1,5 +1,6 @@ import { DocumentType, modelOptions, prop } from '@typegoose/typegoose'; import { CollectionName } from '../../helpers/CollectionName'; +import { MailingConfiguration } from '../../module/settings/model/MailingConfiguration'; import { ClientSettingsDTO } from '../../module/settings/settings.dto'; import { IClientSettings } from '../../shared/model/Settings'; @@ -15,11 +16,15 @@ export class SettingsModel { @prop({ required: true }) canTutorExcuseStudents: boolean; + @prop() + mailingConfig?: MailingConfiguration; + constructor(fields?: Partial) { this.defaultTeamSize = fields?.defaultTeamSize ?? SettingsModel.internalDefaults.defaultTeamSize; this.canTutorExcuseStudents = fields?.canTutorExcuseStudents ?? SettingsModel.internalDefaults.canTutorExcuseStudents; + this.mailingConfig = fields?.mailingConfig; } toDTO(): IClientSettings { diff --git a/server/src/module/settings/model/ApplicationConfiguration.ts b/server/src/module/settings/model/ApplicationConfiguration.ts index d1e0f431a..9f6d26c32 100644 --- a/server/src/module/settings/model/ApplicationConfiguration.ts +++ b/server/src/module/settings/model/ApplicationConfiguration.ts @@ -2,7 +2,6 @@ import { Type } from 'class-transformer'; import { IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator'; import { ClientSettingsDTO } from '../settings.dto'; import { DatabaseConfiguration } from './DatabaseConfiguration'; -import { MailingConfiguration } from './MailingConfiguration'; export class ApplicationConfiguration { @IsOptional() @@ -18,10 +17,6 @@ export class ApplicationConfiguration { @ValidateNested() readonly database!: DatabaseConfiguration; - @Type(() => MailingConfiguration) - @ValidateNested() - readonly mailing!: MailingConfiguration; - @IsOptional() @Type(() => ClientSettingsDTO) @ValidateNested() diff --git a/server/src/module/settings/model/MailingConfiguration.ts b/server/src/module/settings/model/MailingConfiguration.ts index 4d1aff6de..f104f9bf4 100644 --- a/server/src/module/settings/model/MailingConfiguration.ts +++ b/server/src/module/settings/model/MailingConfiguration.ts @@ -1,7 +1,8 @@ import { Type } from 'class-transformer'; -import { IsBoolean, IsNumber, IsString, ValidateNested } from 'class-validator'; +import { IsNumber, IsString, ValidateNested } from 'class-validator'; +import { IMailingAuthConfiguration, IMailingSettings } from '../../../shared/model/Settings'; -export class MailingAuthConfiguration { +export class MailingAuthConfiguration implements IMailingAuthConfiguration { @IsString() readonly user!: string; @@ -9,16 +10,16 @@ export class MailingAuthConfiguration { readonly pass!: string; } -export class MailingConfiguration { - @IsBoolean() - readonly testingMode!: boolean; - +export class MailingConfiguration implements IMailingSettings { @IsString() readonly host!: string; @IsNumber() readonly port!: number; + @IsString() + readonly from!: string; + @ValidateNested() @Type(() => MailingAuthConfiguration) readonly auth!: MailingAuthConfiguration; diff --git a/server/src/module/settings/settings.service.ts b/server/src/module/settings/settings.service.ts index 4608d52a2..9ef5a9fad 100644 --- a/server/src/module/settings/settings.service.ts +++ b/server/src/module/settings/settings.service.ts @@ -4,6 +4,7 @@ import { InjectModel } from 'nestjs-typegoose'; import { SettingsDocument, SettingsModel } from '../../database/models/settings.model'; import { StartUpException } from '../../exceptions/StartUpException'; import { IClientSettings } from '../../shared/model/Settings'; +import { MailingConfiguration } from './model/MailingConfiguration'; import { ClientSettingsDTO } from './settings.dto'; import { StaticSettings } from './settings.static'; @@ -43,6 +44,14 @@ export class SettingsService extends StaticSettings implements OnModuleInit { await document.save(); } + /** + * @returns MailingConfiguration saved in the DB or `undefined` if none are saved. + */ + async getMailingOptions(): Promise { + const document = await this.getSettingsDocument(); + return document.mailingConfig; + } + /** * Checks if there is a `SettingsDocument` in the database. * diff --git a/server/src/module/settings/settings.static.ts b/server/src/module/settings/settings.static.ts index 0ef43f799..e4fb0b9b3 100644 --- a/server/src/module/settings/settings.static.ts +++ b/server/src/module/settings/settings.static.ts @@ -8,7 +8,6 @@ import { StartUpException } from '../../exceptions/StartUpException'; import { ApplicationConfiguration } from './model/ApplicationConfiguration'; import { DatabaseConfiguration } from './model/DatabaseConfiguration'; import { EnvironmentConfig, ENV_VARIABLE_NAMES } from './model/EnvironmentConfig'; -import { MailingConfiguration } from './model/MailingConfiguration'; type DatabaseConfig = DatabaseConfiguration & { secret: string }; @@ -121,13 +120,6 @@ export class StaticSettings { return StaticSettings.STATIC_FOLDER; } - /** - * @returns Configuration for the mailing service. - */ - getMailingConfiguration(): MailingConfiguration { - return this.config.mailing; - } - /** * Loads the configuration of the database. * diff --git a/server/src/shared/model/Mail.ts b/server/src/shared/model/Mail.ts index da1d56817..0f11b45d3 100644 --- a/server/src/shared/model/Mail.ts +++ b/server/src/shared/model/Mail.ts @@ -1,5 +1,6 @@ export interface FailedMail { userId: string; + reason: string; } export interface MailingStatus { diff --git a/server/src/shared/model/Settings.ts b/server/src/shared/model/Settings.ts index 688f725d5..05f66acd6 100644 --- a/server/src/shared/model/Settings.ts +++ b/server/src/shared/model/Settings.ts @@ -1,4 +1,17 @@ +export interface IMailingAuthConfiguration { + user: string; + pass: string; +} + +export interface IMailingSettings { + from: string; + host: string; + port: number; + auth: IMailingAuthConfiguration; +} + export interface IClientSettings { defaultTeamSize: number; canTutorExcuseStudents: boolean; + mailingConfig?: IMailingSettings; } From 3524126ff534436a19eed8859cd1c101a58affd0 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Wed, 29 Jul 2020 23:46:57 +0200 Subject: [PATCH 39/62] Change MailingService to use the new settings from the SettingsModel. This also changes a lot of the logic in this service and cleans it up. This way it is more maintainable and readable (and hopefully less error prone). --- server/src/module/mail/mail.service.ts | 303 ++++++++++--------------- 1 file changed, 124 insertions(+), 179 deletions(-) diff --git a/server/src/module/mail/mail.service.ts b/server/src/module/mail/mail.service.ts index ecdb0fe37..b7b485bd7 100644 --- a/server/src/module/mail/mail.service.ts +++ b/server/src/module/mail/mail.service.ts @@ -1,31 +1,23 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; import nodemailer from 'nodemailer'; import Mail from 'nodemailer/lib/mailer'; -import SMTPConnection, { - AuthenticationTypeLogin, - AuthenticationTypeOAuth2, -} from 'nodemailer/lib/smtp-connection'; -import SMTPTransport from 'nodemailer/lib/smtp-transport'; -import { MailingStatus, FailedMail } from '../../shared/model/Mail'; -import { UserService } from '../user/user.service'; +import { SentMessageInfo } from 'nodemailer/lib/smtp-transport'; import { UserDocument } from '../../database/models/user.model'; +import { FailedMail, MailingStatus } from '../../shared/model/Mail'; +import { getNameOfEntity } from '../../shared/util/helpers'; +import { MailingConfiguration } from '../settings/model/MailingConfiguration'; import { SettingsService } from '../settings/settings.service'; import { TemplateService } from '../template/template.service'; -import { getNameOfEntity } from '../../shared/util/helpers'; - -interface AdditionalOptions { - testingMode?: boolean; -} +import { UserService } from '../user/user.service'; class MailingError { - constructor(readonly userId: string, readonly err: Error, readonly message: string) {} + constructor(readonly userId: string, readonly message: string, readonly err: unknown) {} } -type MailingResponse = SMTPTransport.SentMessageInfo | MailingError; -type TransportOptions = SMTPTransport.Options & AdditionalOptions; - -export class InvalidConfigurationException { - constructor(readonly message?: string) {} +interface SendMailParams { + user: UserDocument; + transport: Mail; + options: MailingConfiguration; } @Injectable() @@ -34,34 +26,42 @@ export class MailService { constructor( private readonly userService: UserService, - private readonly settings: SettingsService, + private readonly settingsService: SettingsService, private readonly templateService: TemplateService ) {} /** - * Sends a mail with the credentials to all users with their credentials. + * Sends a mail with the credentials to all users. * - * The mail is only sent to users which are not the 'admin' user and which did not already changed their initial password. + * The mail is only sent to users which are not the 'admin' user and which did not already changed their initial password (ie still have a temporary password). * * @returns Data containing information about the amount of successfully send mails and information about failed ones. */ async mailCredentials(): Promise { - const options = this.getConfig(); - const smtpTransport = nodemailer.createTransport(options); + const options = await this.settingsService.getMailingOptions(); - const users = await this.userService.findAll(); - const mails: Promise[] = []; - const userToSendMailTo = users.filter((u) => u.username !== 'admin' && !!u.temporaryPassword); + if (!options) { + throw new InternalServerErrorException('MISSING_MAIL_SETTINGS'); + } + + const transport = this.createSMTPTransport(options); + const usersToMail = (await this.userService.findAll()).filter( + (u) => u.username !== 'admin' && !!u.temporaryPassword + ); + const mails: Promise[] = []; + const failedMails: FailedMail[] = []; - for (const user of userToSendMailTo) { + for (const user of usersToMail) { if (this.isValidEmail(user.email)) { - mails.push(this.sendMail(user, smtpTransport, options)); + mails.push(this.sendMail({ user, transport, options })); + } else { + failedMails.push({ userId: user.id, reason: 'INVALID_EMAIL_ADDRESS' }); } } - const status = this.generateMailingStatus(await Promise.all(mails), users); + const status = this.generateMailingStatus(await Promise.allSettled(mails)); + transport.close(); - smtpTransport.close(); return status; } @@ -73,184 +73,129 @@ export class MailService { * @returns Data containing information about the amount of successfully send mails (1) and information on failure. */ async mailSingleCredentials(userId: string): Promise { - const options = this.getConfig(); - const smtpTransport = nodemailer.createTransport(options); - - const user = await this.userService.findById(userId); + const options = await this.settingsService.getMailingOptions(); - if (!user.temporaryPassword || !this.isValidEmail(user.email)) { - return { successFullSend: 0, failedMailsInfo: [{ userId: user.id }] }; + if (!options) { + throw new InternalServerErrorException('MISSING_MAIL_SETTINGS'); } - const status = await this.sendMail(user, smtpTransport, options); - - smtpTransport.close(); - - return this.generateMailingStatus([status], [user]); - } + const transport = this.createSMTPTransport(options); + const user = await this.userService.findById(userId); - private async sendMail( - user: UserDocument, - transport: Mail, - options: TransportOptions - ): Promise { - try { - return await transport.sendMail({ - from: this.getUser(), - to: `${user.email}`, - subject: 'Credentials', - text: this.getTextOfMail(user), - envelope: { - to: `${user.email}`, - ...options.envelope, - }, - }); - } catch (err) { - return new MailingError(user.id, err, 'Could not send mail.'); + if (!user.temporaryPassword) { + return { + successFullSend: 0, + failedMailsInfo: [{ userId: user.id, reason: 'NO_TEMP_PWD_ON_USER' }], + }; } - } - private generateMailingStatus(mails: MailingResponse[], users: UserDocument[]): MailingStatus { - const failedMailsInfo: FailedMail[] = []; - let successFullSend: number = 0; + if (!this.isValidEmail(user.email)) { + return { + successFullSend: 0, + failedMailsInfo: [{ userId: user.id, reason: 'INVALID_EMAIL_ADDRESS' }], + }; + } - for (const mail of mails) { - if (mail instanceof MailingError) { - const user = users.find((u) => u.id === mail.userId) as UserDocument; + const status = await Promise.allSettled([this.sendMail({ user, transport, options })]); + transport.close(); - failedMailsInfo.push({ - userId: user.id, - }); + return this.generateMailingStatus(status); + } - this.logger.error(`${mail.message} -- ${mail.err}`); + /** + * Converts the given promise results into a `MailingStatus` object extracting the important information: + * + * - Information about all failed ones. + * - Amount of successfully sent ones. + * + * @param promiseResults All promise results from settled promises sending mails. + * @returns `MailingStatus` according to the responses. + */ + private generateMailingStatus( + promiseResults: PromiseSettledResult[] + ): MailingStatus { + const status: MailingStatus = { failedMailsInfo: [], successFullSend: 0 }; + + for (const mail of promiseResults) { + if (mail.status === 'fulfilled') { + status.successFullSend += 1; } else { - const previewURL = nodemailer.getTestMessageUrl(mail); - - if (previewURL) { - this.logger.log(`Mail successfully send. Preview: ${previewURL}`); + if (mail.reason instanceof MailingError) { + status.failedMailsInfo.push({ + userId: mail.reason.userId, + reason: `${mail.reason.message}:\n${JSON.stringify(mail.reason.err, null, 2)}`, + }); } else { - this.logger.log('Mail successfully send.'); + status.failedMailsInfo.push({ userId: 'UNKNOWN', reason: 'UNKNOWN_ERROR' }); } - - successFullSend++; } } - return { successFullSend, failedMailsInfo }; + return status; + } + + /** + * Tries to send a mail with the credentials of the given user. + * + * If the mail was sent successfully a `SentMessageInfo` is returned. If it fails an error is thrown. + * + * @param user User to send the mail to + * @param options MailingConfiguration + * @param transport Transport to use. + * + * @returns Info about the sent message. + * @throws `MailingError` - If the mail could not be successfully send. + */ + private async sendMail({ user, options, transport }: SendMailParams): Promise { + try { + return await transport.sendMail({ + from: options.from, + to: user.email, + subject: 'TMS Credentials', // TODO: Make configurable + html: this.getTextOfMail(user), + }); + } catch (err) { + throw new MailingError(user.id, 'SEND_MAIL_FAILED', err); + } } + /** + * @param user User to get the mail text for. + * @returns The mail template filled with the data of the given user + */ private getTextOfMail(user: UserDocument): string { const template = this.templateService.getMailTemplate(); - return template({ name: getNameOfEntity(user, { firstNameFirst: true }), username: user.username, - password: user.temporaryPassword ?? '', + password: user.temporaryPassword ?? 'NO_TMP_PASSWORD', }); } - private getConfig(): TransportOptions { - const options = this.settings.getMailingConfiguration(); - - if (options.testingMode) { - const auth = options.auth as AuthenticationTypeLogin | undefined; - - if (!auth || !auth.user || !auth.pass) { - throw new InvalidConfigurationException( - 'In testing mode an ethereal user & pass has to be supplied.' - ); - } - - if (!auth.user.includes('ethereal')) { - throw new InvalidConfigurationException( - 'In testing mode mailing user has to be an ethereal user.' - ); - } - - return { - host: 'smtp.ethereal.email', - port: 587, - auth: { - user: auth.user, - pass: auth.pass, - }, - }; - } else { - this.assertValidConfig(options); - - return options; - } - } - - private getUser(): string { - const options = this.getConfig(); - - if (options.from) { - if (typeof options.from === 'string') { - return options.from; - } - - if (options.from.address) { - return options.from.address; - } - } - - if (options.auth && options.auth.user) { - if (this.isValidEmail(options.auth.user)) { - return options.auth.user; - } - } - - throw new InvalidConfigurationException( - "Mailing must contain either a 'from' prop which contains an email address or the 'auth.user' must be a valid email address" - ); - } - - private assertValidConfig(config: TransportOptions) { - if (!config.auth) { - throw new InvalidConfigurationException('No authentication settings were provided'); - } - - if (this.isOAuth2(config.auth)) { - const { user, clientId, clientSecret, refreshToken } = config.auth; - - if (!user || !clientId || !clientSecret || !refreshToken) { - throw new InvalidConfigurationException( - 'user & clientId & clientSecret & refreshToken all have to be set in mailing auth options.' - ); - } - - return; - } - - if (this.isBasicAuth(config.auth)) { - const { user, pass } = config.auth; - - if (!user || !pass) { - throw new InvalidConfigurationException( - 'user && pass all have to be set in mailing auth options' - ); - } - - return; - } - - throw new InvalidConfigurationException( - "Authentication type has to be 'OAuth2' or 'Login' (or undefined which defaults to 'Login')." - ); - } - - private isOAuth2(auth: SMTPConnection.AuthenticationType): auth is AuthenticationTypeOAuth2 { - return auth.type === 'oauth2' || auth.type === 'OAuth2' || auth.type === 'OAUTH2'; - } - - private isBasicAuth(auth: SMTPConnection.AuthenticationType): auth is AuthenticationTypeLogin { - return !auth.type || auth.type === 'login' || auth.type === 'Login' || auth.type === 'LOGIN'; + /** + * @param options Options to create the transport with. + * @returns A nodemail SMTPTransport instance created with the given options. + */ + private createSMTPTransport(options: MailingConfiguration): Mail { + // TODO: Add testingMode with ethereal! + return nodemailer.createTransport(options); } + /** + * @param email String to check. + * @returns Is the string a valid email address? + */ private isValidEmail(email: string): boolean { return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( email ); } + + // private isOAuth2(auth: SMTPConnection.AuthenticationType): auth is AuthenticationTypeOAuth2 { + // return auth.type === 'oauth2' || auth.type === 'OAuth2' || auth.type === 'OAUTH2'; + // } + + // private isBasicAuth(auth: SMTPConnection.AuthenticationType): auth is AuthenticationTypeLogin { + // return !auth.type || auth.type === 'login' || auth.type === 'Login' || auth.type === 'LOGIN'; + // } } From 5857170242a0256c67f3caa73768a5a20e3883ec Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 10:30:07 +0200 Subject: [PATCH 40/62] Change settings dtos to include a mailingConfig property. --- server/src/database/models/settings.model.ts | 1 + server/src/module/settings/settings.dto.ts | 13 ++++++++++--- server/src/shared/model/Settings.ts | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index e6b18c722..93256102e 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -33,6 +33,7 @@ export class SettingsModel { return { defaultTeamSize: this.defaultTeamSize ?? defaultSettings.defaultTeamSize, canTutorExcuseStudents: this.canTutorExcuseStudents ?? defaultSettings.canTutorExcuseStudents, + mailingConfig: this.mailingConfig, }; } diff --git a/server/src/module/settings/settings.dto.ts b/server/src/module/settings/settings.dto.ts index 2de085fc5..bfbe3b09c 100644 --- a/server/src/module/settings/settings.dto.ts +++ b/server/src/module/settings/settings.dto.ts @@ -1,7 +1,9 @@ -import { IsBoolean, IsNumber, IsOptional, Min } from 'class-validator'; -import { IClientSettings } from '../../shared/model/Settings'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsNumber, IsOptional, Min, ValidateNested } from 'class-validator'; +import { IChangeSettingsDTO } from '../../shared/model/Settings'; +import { MailingConfiguration } from './model/MailingConfiguration'; -export class ClientSettingsDTO implements Partial { +export class ClientSettingsDTO implements IChangeSettingsDTO { @IsNumber() @Min(1) @IsOptional() @@ -10,4 +12,9 @@ export class ClientSettingsDTO implements Partial { @IsBoolean() @IsOptional() canTutorExcuseStudents?: boolean; + + @IsOptional() + @Type(() => MailingConfiguration) + @ValidateNested() + mailingConfig?: MailingConfiguration; } diff --git a/server/src/shared/model/Settings.ts b/server/src/shared/model/Settings.ts index 05f66acd6..12a21c793 100644 --- a/server/src/shared/model/Settings.ts +++ b/server/src/shared/model/Settings.ts @@ -15,3 +15,5 @@ export interface IClientSettings { canTutorExcuseStudents: boolean; mailingConfig?: IMailingSettings; } + +export type IChangeSettingsDTO = { [K in keyof IClientSettings]?: IClientSettings[K] }; From 2eb7cf2a985017eb6be0672f8b5635a767115c5b Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 10:54:26 +0200 Subject: [PATCH 41/62] Add proper model for mailing settings. --- server/src/database/models/settings.model.ts | 37 +++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index 93256102e..2f474558d 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -1,8 +1,35 @@ import { DocumentType, modelOptions, prop } from '@typegoose/typegoose'; import { CollectionName } from '../../helpers/CollectionName'; -import { MailingConfiguration } from '../../module/settings/model/MailingConfiguration'; import { ClientSettingsDTO } from '../../module/settings/settings.dto'; -import { IClientSettings } from '../../shared/model/Settings'; +import { + IClientSettings, + IMailingAuthConfiguration, + IMailingSettings, +} from '../../shared/model/Settings'; + +@modelOptions({ schemaOptions: { _id: false } }) +class MailingAuthModel implements IMailingAuthConfiguration { + @prop({ required: true }) + readonly user!: string; + + @prop({ required: true }) + readonly pass!: string; +} + +@modelOptions({ schemaOptions: { _id: false } }) +class MailingSettingsModel implements IMailingSettings { + @prop({ required: true }) + readonly host!: string; + + @prop({ required: true }) + readonly port!: number; + + @prop({ required: true }) + readonly from!: string; + + @prop({ required: true, type: MailingAuthModel }) + readonly auth!: MailingAuthModel; +} @modelOptions({ schemaOptions: { collection: CollectionName.SETTINGS } }) export class SettingsModel { @@ -16,8 +43,8 @@ export class SettingsModel { @prop({ required: true }) canTutorExcuseStudents: boolean; - @prop() - mailingConfig?: MailingConfiguration; + @prop({ type: MailingSettingsModel }) + mailingConfig?: MailingSettingsModel; constructor(fields?: Partial) { this.defaultTeamSize = @@ -47,7 +74,7 @@ export class SettingsModel { assignDTO(dto: ClientSettingsDTO): void { for (const [key, value] of Object.entries(dto)) { if (typeof value !== 'function' && key in this) { - (this as Record)[key] = value; + (this as Record)[key] = value ?? undefined; } } } From 0027547843db7e32f28a19c561d6d332911ea033 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 10:54:58 +0200 Subject: [PATCH 42/62] Add more validation to the mailing settings. --- .../validators/nodemailer.validator.ts | 47 +++++++++++++++++++ server/src/module/mail/mail.service.ts | 5 +- .../settings/model/MailingConfiguration.ts | 7 ++- 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 server/src/helpers/validators/nodemailer.validator.ts diff --git a/server/src/helpers/validators/nodemailer.validator.ts b/server/src/helpers/validators/nodemailer.validator.ts new file mode 100644 index 000000000..243a221a9 --- /dev/null +++ b/server/src/helpers/validators/nodemailer.validator.ts @@ -0,0 +1,47 @@ +import { registerDecorator, ValidationOptions } from 'class-validator'; + +export const VALID_EMAIL_REGEX = /(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z-0-9]+\.)+[a-zA-Z]{2,}))/u; + +/** + * Validates the property to be a valid mail sender for nodemailer. + * + * N + * + * @param validationOptions Options passed to the class-validator. + */ +export function IsValidMailSender(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + const message: any = { + message: validationOptions?.each + ? `each sender in ${propertyName} must be in a valid sender format ('email' or 'name ') or a comma seperated list of those.` + : `${propertyName} must be in a valid sender format ('email' or 'name ') or a comma seperated list of those.`, + }; + + registerDecorator({ + name: 'isValidMailSender', + target: object.constructor, + propertyName, + options: { ...message, ...validationOptions }, + validator: { + validate(value: any): boolean { + if (typeof value !== 'string' || !value) { + return false; + } + + const mail = VALID_EMAIL_REGEX.source; + const name = /([\p{L}\p{N}",*-]|[^\S\r\n])+/.source; + const regex = new RegExp(`${mail}|(${name} <${mail}>)`, 'u'); + const subStrings = value.split(','); + + for (const str of subStrings) { + if (!regex.test(str.trim())) { + return false; + } + } + + return true; + }, + }, + }); + }; +} diff --git a/server/src/module/mail/mail.service.ts b/server/src/module/mail/mail.service.ts index b7b485bd7..910d15a2b 100644 --- a/server/src/module/mail/mail.service.ts +++ b/server/src/module/mail/mail.service.ts @@ -3,6 +3,7 @@ import nodemailer from 'nodemailer'; import Mail from 'nodemailer/lib/mailer'; import { SentMessageInfo } from 'nodemailer/lib/smtp-transport'; import { UserDocument } from '../../database/models/user.model'; +import { VALID_EMAIL_REGEX } from '../../helpers/validators/nodemailer.validator'; import { FailedMail, MailingStatus } from '../../shared/model/Mail'; import { getNameOfEntity } from '../../shared/util/helpers'; import { MailingConfiguration } from '../settings/model/MailingConfiguration'; @@ -186,9 +187,7 @@ export class MailService { * @returns Is the string a valid email address? */ private isValidEmail(email: string): boolean { - return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - email - ); + return VALID_EMAIL_REGEX.test(email); } // private isOAuth2(auth: SMTPConnection.AuthenticationType): auth is AuthenticationTypeOAuth2 { diff --git a/server/src/module/settings/model/MailingConfiguration.ts b/server/src/module/settings/model/MailingConfiguration.ts index f104f9bf4..f26a0ae4f 100644 --- a/server/src/module/settings/model/MailingConfiguration.ts +++ b/server/src/module/settings/model/MailingConfiguration.ts @@ -1,9 +1,11 @@ import { Type } from 'class-transformer'; -import { IsNumber, IsString, ValidateNested } from 'class-validator'; +import { IsNotEmpty, IsNumber, IsObject, IsString, ValidateNested } from 'class-validator'; +import { IsValidMailSender } from '../../../helpers/validators/nodemailer.validator'; import { IMailingAuthConfiguration, IMailingSettings } from '../../../shared/model/Settings'; export class MailingAuthConfiguration implements IMailingAuthConfiguration { @IsString() + @IsNotEmpty() readonly user!: string; @IsString() @@ -12,14 +14,17 @@ export class MailingAuthConfiguration implements IMailingAuthConfiguration { export class MailingConfiguration implements IMailingSettings { @IsString() + @IsNotEmpty() readonly host!: string; @IsNumber() readonly port!: number; @IsString() + @IsValidMailSender() readonly from!: string; + @IsObject() @ValidateNested() @Type(() => MailingAuthConfiguration) readonly auth!: MailingAuthConfiguration; From fdf74902d5c0940a74a7092b7c2bbeff3672cc36 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 12:43:31 +0200 Subject: [PATCH 43/62] Add encryption to MailingSettingsModel. --- server/src/database/models/settings.model.ts | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index 2f474558d..6d35fffe3 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -1,6 +1,8 @@ -import { DocumentType, modelOptions, prop } from '@typegoose/typegoose'; +import { DocumentType, modelOptions, plugin, prop } from '@typegoose/typegoose'; +import { fieldEncryption } from 'mongoose-field-encryption'; import { CollectionName } from '../../helpers/CollectionName'; import { ClientSettingsDTO } from '../../module/settings/settings.dto'; +import { StaticSettings } from '../../module/settings/settings.static'; import { IClientSettings, IMailingAuthConfiguration, @@ -16,6 +18,10 @@ class MailingAuthModel implements IMailingAuthConfiguration { readonly pass!: string; } +@plugin(fieldEncryption, { + secret: StaticSettings.getService().getDatabaseSecret(), + fields: ['host', 'port', 'from', 'auth'], +}) @modelOptions({ schemaOptions: { _id: false } }) class MailingSettingsModel implements IMailingSettings { @prop({ required: true }) @@ -29,6 +35,15 @@ class MailingSettingsModel implements IMailingSettings { @prop({ required: true, type: MailingAuthModel }) readonly auth!: MailingAuthModel; + + constructor(fields?: IMailingSettings) { + Object.assign(this, fields); + } + + toDTO(): IMailingSettings { + const { host, port, from, auth } = this; + return { host, port, from, auth }; + } } @modelOptions({ schemaOptions: { collection: CollectionName.SETTINGS } }) @@ -51,7 +66,7 @@ export class SettingsModel { fields?.defaultTeamSize ?? SettingsModel.internalDefaults.defaultTeamSize; this.canTutorExcuseStudents = fields?.canTutorExcuseStudents ?? SettingsModel.internalDefaults.canTutorExcuseStudents; - this.mailingConfig = fields?.mailingConfig; + this.mailingConfig = new MailingSettingsModel(fields?.mailingConfig); } toDTO(): IClientSettings { @@ -60,7 +75,7 @@ export class SettingsModel { return { defaultTeamSize: this.defaultTeamSize ?? defaultSettings.defaultTeamSize, canTutorExcuseStudents: this.canTutorExcuseStudents ?? defaultSettings.canTutorExcuseStudents, - mailingConfig: this.mailingConfig, + mailingConfig: this.mailingConfig?.toDTO(), }; } From 837747864e4f1abe706a65ca24add3bce035e676 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 13:09:39 +0200 Subject: [PATCH 44/62] Add FormikPasswordField component. --- client/src/components/forms/UserForm.tsx | 20 +++--------- .../forms/components/FormikPasswordField.tsx | 32 +++++++++++++++++++ .../components/FormikTextFieldWithButtons.tsx | 4 +-- 3 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 client/src/components/forms/components/FormikPasswordField.tsx diff --git a/client/src/components/forms/UserForm.tsx b/client/src/components/forms/UserForm.tsx index 3d8133b97..4a5556f50 100644 --- a/client/src/components/forms/UserForm.tsx +++ b/client/src/components/forms/UserForm.tsx @@ -1,17 +1,14 @@ import { FormikHelpers } from 'formik'; import pwGenerator from 'generate-password'; -import { - Eye as RemoveRedEyeOutlinedIcon, - Restore as RestoreOutlinedIcon, - Shuffle as ShuffleIcon, -} from 'mdi-material-ui'; -import React, { useState } from 'react'; +import { Restore as RestoreOutlinedIcon, Shuffle as ShuffleIcon } from 'mdi-material-ui'; +import React from 'react'; import { Role } from 'shared/model/Role'; import { IUser } from 'shared/model/User'; import * as Yup from 'yup'; import { Tutorial } from '../../model/Tutorial'; import { FormikSubmitCallback } from '../../types'; import { passwordValidationSchema } from '../../util/validationSchemas'; +import FormikPasswordField from './components/FormikPasswordField'; import FormikSelect from './components/FormikSelect'; import FormikTextField from './components/FormikTextField'; import { FormikTextFieldWithButtons } from './components/FormikTextFieldWithButtons'; @@ -120,8 +117,6 @@ function UserForm({ className, ...other }: Props): JSX.Element { - const [hidePassword, setHidePassword] = useState(true); - const isEditMode = user !== undefined; const ValidationSchema = getValidationSchema(availableRoles, isEditMode); const initialFormState: UserFormState = getInitialFormState(user); @@ -213,18 +208,11 @@ function UserForm({ ]} /> - setHidePassword(!hidePassword), - }, { key: 'generatePassword', Icon: ShuffleIcon, diff --git a/client/src/components/forms/components/FormikPasswordField.tsx b/client/src/components/forms/components/FormikPasswordField.tsx new file mode 100644 index 000000000..208fe3766 --- /dev/null +++ b/client/src/components/forms/components/FormikPasswordField.tsx @@ -0,0 +1,32 @@ +import { Eye as RemoveRedEyeOutlinedIcon } from 'mdi-material-ui'; +import React, { useState } from 'react'; +import { + FormikTextFieldWithButtons, + FormikTextFieldWithButtonsProps, +} from './FormikTextFieldWithButtons'; + +type PropTypes = Omit & { + buttons?: FormikTextFieldWithButtonsProps['buttons']; +}; + +function FormikPasswordField({ buttons, ...props }: PropTypes): JSX.Element { + const [hidePassword, setHidePassword] = useState(true); + + return ( + setHidePassword(!hidePassword), + }, + ...(buttons ?? []), + ]} + /> + ); +} + +export default FormikPasswordField; diff --git a/client/src/components/forms/components/FormikTextFieldWithButtons.tsx b/client/src/components/forms/components/FormikTextFieldWithButtons.tsx index c6dd6d520..335276631 100644 --- a/client/src/components/forms/components/FormikTextFieldWithButtons.tsx +++ b/client/src/components/forms/components/FormikTextFieldWithButtons.tsx @@ -42,7 +42,7 @@ interface Props { DivProps?: React.ComponentProps<'div'>; } -type PropType = Props & Omit; +export type FormikTextFieldWithButtonsProps = Props & Omit; export function FormikTextFieldWithButtons({ name, @@ -51,7 +51,7 @@ export function FormikTextFieldWithButtons({ DivProps, disabled, ...TextFieldProps -}: PropType): JSX.Element { +}: FormikTextFieldWithButtonsProps): JSX.Element { const classes = useStyles(); const buttonComps = buttons.map(({ key, Icon, onClick, color, tooltip }) => { const buttonComp = ( From ebac56b171880b091ec995285a6eda5040fb6a5a Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 17:48:31 +0200 Subject: [PATCH 45/62] Add e-mail settings to the settings page. --- client/src/pages/settings/SettingsPage.tsx | 251 +++++++++++++----- .../settings/components/EMailSettings.tsx | 70 +++++ 2 files changed, 253 insertions(+), 68 deletions(-) create mode 100644 client/src/pages/settings/components/EMailSettings.tsx diff --git a/client/src/pages/settings/SettingsPage.tsx b/client/src/pages/settings/SettingsPage.tsx index 82eb9e567..3f0653d55 100644 --- a/client/src/pages/settings/SettingsPage.tsx +++ b/client/src/pages/settings/SettingsPage.tsx @@ -1,17 +1,20 @@ -import { Box, Divider, Typography } from '@material-ui/core'; +import { Box, Button, Divider, Typography } from '@material-ui/core'; import { createStyles, makeStyles } from '@material-ui/core/styles'; -import { Formik } from 'formik'; +import { Formik, useFormikContext } from 'formik'; import React, { useCallback, useMemo } from 'react'; import { IClientSettings } from 'shared/model/Settings'; import * as Yup from 'yup'; import FormikCheckbox from '../../components/forms/components/FormikCheckbox'; +import FormikDebugDisplay from '../../components/forms/components/FormikDebugDisplay'; import FormikTextField from '../../components/forms/components/FormikTextField'; import SubmitButton from '../../components/loading/SubmitButton'; import Placeholder from '../../components/Placeholder'; +import { useDialog } from '../../hooks/DialogService'; import { setSettings } from '../../hooks/fetching/Settings'; import { useCustomSnackbar } from '../../hooks/snackbar/useCustomSnackbar'; import { useSettings } from '../../hooks/useSettings'; import { FormikSubmitCallback } from '../../types'; +import EMailSettings from './components/EMailSettings'; const useStyles = makeStyles((theme) => createStyles({ @@ -21,40 +24,205 @@ const useStyles = makeStyles((theme) => }) ); -const validationSchema = Yup.object().shape({ +const validationSchema = Yup.object().shape({ canTutorExcuseStudents: Yup.boolean().required('Benötigt'), defaultTeamSize: Yup.number() .integer('Muss eine ganze Zahl sein.') .min(1, 'Muss mindestens 1 sein.') .required('Benötigt'), + mailingConfig: Yup.lazy((value: { enabled?: boolean } | undefined | null) => { + if (value?.enabled) { + return Yup.object().shape({ + enabled: Yup.boolean().defined(), + from: Yup.string() + .required('Benötigt') + .test({ + name: 'isValidFrom', + message: 'Muss eine kommaseparierte Liste mit "{email}" oder "{name} <{email}>" sein', + test: (value) => { + if (typeof value !== 'string') { + return false; + } + + const regexMail = /[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*/; + const regexName = /([\p{L}\p{N}",*-]|[^\S\r\n])+/; + const regex = new RegExp( + `^(${regexMail.source})|(${regexName.source} <${regexMail.source}>)$`, + 'u' + ); + const mails = value.split(',').map((m) => m.trim()); + + for (const mail of mails) { + if (!regex.test(mail)) { + return false; + } + } + + return true; + }, + }), + host: Yup.string().required('Benötigt'), + port: Yup.number() + .positive('Muss eine positive Zahl sein') + .integer('Muss eine ganze Zahl sein') + .required('Benötigt'), + auth: Yup.object().shape({ + user: Yup.string().required('Benötigt.'), + pass: Yup.string().required('Benötigt.'), + }), + }); + } else { + return Yup.mixed(); + } + }), }); interface FormState { defaultTeamSize: string; canTutorExcuseStudents: boolean; + mailingConfig: { + enabled: boolean; + from: string; + host: string; + port: string; + auth: { user: string; pass: string }; + }; +} + +function getInitialValues(settings: IClientSettings): FormState { + const { mailingConfig, canTutorExcuseStudents, defaultTeamSize } = settings; + return { + canTutorExcuseStudents: canTutorExcuseStudents, + defaultTeamSize: `${defaultTeamSize}`, + mailingConfig: { + enabled: !!mailingConfig, + from: mailingConfig?.from ?? '', + host: mailingConfig?.host ?? '', + port: `${mailingConfig?.port ?? ''}`, + auth: { user: mailingConfig?.auth.user ?? '', pass: mailingConfig?.auth.pass ?? '' }, + }, + }; +} + +function convertFormStateToDTO(values: FormState): IClientSettings { + const dto: IClientSettings = { + canTutorExcuseStudents: values.canTutorExcuseStudents, + defaultTeamSize: Number.parseInt(values.defaultTeamSize), + }; + + if (values.mailingConfig.enabled) { + const { from, host, port, auth } = values.mailingConfig; + dto.mailingConfig = { + from, + host, + port: Number.parseInt(port), + auth: { user: auth.user, pass: auth.pass }, + }; + } + + return dto; } +// TODO: Extract me?! function GridDivider(): JSX.Element { return ; } -function SettingsPage(): JSX.Element { +function SettingsPageForm(): JSX.Element { const classes = useStyles(); + const { showConfirmationDialog } = useDialog(); + const { handleSubmit, isSubmitting, dirty, isValid, resetForm } = useFormikContext(); + + const handleFormReset = useCallback(async () => { + const result = await showConfirmationDialog({ + title: 'Einstellungen zurücksetzen?', + content: + 'Sollen die Einstellungen wirklich zurückgesetzt werden? Dies kann nicht rückgängig gemacht werden!', + acceptProps: { label: 'Zurücksetzen', deleteButton: true }, + cancelProps: { label: 'Abbrechen' }, + }); + + if (result) { + resetForm(); + } + }, [showConfirmationDialog, resetForm]); + + return ( +
+ + + Einstellungen speichern + + + {dirty && ( + + Es gibt ungespeicherte Änderungen. + + )} + + + + + + Standardteamgröße + + + + + Anwesenheiten + + + + + E-Maileinstellungen + + + + + + ); +} + +function SettingsPage(): JSX.Element { const { isLoadingSettings, settings, updateSettings } = useSettings(); const { promiseWithSnackbar } = useCustomSnackbar(); - const initialValues: FormState = useMemo(() => { - return { - canTutorExcuseStudents: settings.canTutorExcuseStudents, - defaultTeamSize: `${settings.defaultTeamSize}`, - }; - }, [settings]); + const initialValues: FormState = useMemo(() => getInitialValues(settings), [settings]); const onSubmit: FormikSubmitCallback = useCallback( async (values) => { - const dto: IClientSettings = { - canTutorExcuseStudents: values.canTutorExcuseStudents, - defaultTeamSize: Number.parseInt(values.defaultTeamSize), - }; + const dto: IClientSettings = convertFormStateToDTO(values); await promiseWithSnackbar({ promiseFunction: setSettings, @@ -76,60 +244,7 @@ function SettingsPage(): JSX.Element { loading={isLoadingSettings} > - {({ handleSubmit, isValid, dirty, isSubmitting }) => ( -
- - - Einstellungen speichern - - - {dirty && ( - - Es gibt ungespeicherte Änderungen. - - )} - - - - Standardteamgröße - - - - - Anwesenheiten - - - - - E-Maileinstellungen - 🛠 Work in progress - -
- )} +
); diff --git a/client/src/pages/settings/components/EMailSettings.tsx b/client/src/pages/settings/components/EMailSettings.tsx new file mode 100644 index 000000000..168571074 --- /dev/null +++ b/client/src/pages/settings/components/EMailSettings.tsx @@ -0,0 +1,70 @@ +import { Box } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import { useField } from 'formik'; +import React from 'react'; +import FormikCheckbox from '../../../components/forms/components/FormikCheckbox'; +import FormikPasswordField from '../../../components/forms/components/FormikPasswordField'; +import FormikTextField from '../../../components/forms/components/FormikTextField'; + +const useStyles = makeStyles((theme) => + createStyles({ + input: { margin: theme.spacing(1, 0) }, + portInput: { marginLeft: theme.spacing(1) }, + }) +); + +function EMailSettings(): JSX.Element { + const classes = useStyles(); + const [, { value: isEnabled }] = useField('mailingConfig.enabled'); + + return ( + + + + + + + + + + + + + + + ); +} + +export default EMailSettings; From f99e216825052f3e919880a2ce3a815924db89fd Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 17:49:33 +0200 Subject: [PATCH 46/62] Extract GridDivider component from SettingsPage. --- client/src/components/GridDivider.tsx | 8 ++++++++ client/src/pages/settings/SettingsPage.tsx | 8 ++------ 2 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 client/src/components/GridDivider.tsx diff --git a/client/src/components/GridDivider.tsx b/client/src/components/GridDivider.tsx new file mode 100644 index 000000000..2382850bd --- /dev/null +++ b/client/src/components/GridDivider.tsx @@ -0,0 +1,8 @@ +import { Divider } from '@material-ui/core'; +import React from 'react'; + +function GridDivider(): JSX.Element { + return ; +} + +export default GridDivider; diff --git a/client/src/pages/settings/SettingsPage.tsx b/client/src/pages/settings/SettingsPage.tsx index 3f0653d55..1363b6955 100644 --- a/client/src/pages/settings/SettingsPage.tsx +++ b/client/src/pages/settings/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Divider, Typography } from '@material-ui/core'; +import { Box, Button, Typography } from '@material-ui/core'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import { Formik, useFormikContext } from 'formik'; import React, { useCallback, useMemo } from 'react'; @@ -7,6 +7,7 @@ import * as Yup from 'yup'; import FormikCheckbox from '../../components/forms/components/FormikCheckbox'; import FormikDebugDisplay from '../../components/forms/components/FormikDebugDisplay'; import FormikTextField from '../../components/forms/components/FormikTextField'; +import GridDivider from '../../components/GridDivider'; import SubmitButton from '../../components/loading/SubmitButton'; import Placeholder from '../../components/Placeholder'; import { useDialog } from '../../hooks/DialogService'; @@ -123,11 +124,6 @@ function convertFormStateToDTO(values: FormState): IClientSettings { return dto; } -// TODO: Extract me?! -function GridDivider(): JSX.Element { - return ; -} - function SettingsPageForm(): JSX.Element { const classes = useStyles(); const { showConfirmationDialog } = useDialog(); From 988a5a94ebc5d194c5a7b5bad16d6312cf667c7f Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 17:53:53 +0200 Subject: [PATCH 47/62] Split SettingsPage into different files to keep things organized. --- .../pages/settings/SettingsPage.helpers.ts | 101 +++++++++ client/src/pages/settings/SettingsPage.tsx | 213 +----------------- .../settings/components/SettingsPage.form.tsx | 109 +++++++++ 3 files changed, 218 insertions(+), 205 deletions(-) create mode 100644 client/src/pages/settings/SettingsPage.helpers.ts create mode 100644 client/src/pages/settings/components/SettingsPage.form.tsx diff --git a/client/src/pages/settings/SettingsPage.helpers.ts b/client/src/pages/settings/SettingsPage.helpers.ts new file mode 100644 index 000000000..f2fbb0ed6 --- /dev/null +++ b/client/src/pages/settings/SettingsPage.helpers.ts @@ -0,0 +1,101 @@ +import { IClientSettings } from 'shared/model/Settings'; +import * as Yup from 'yup'; + +export const validationSchema = Yup.object().shape({ + canTutorExcuseStudents: Yup.boolean().required('Benötigt'), + defaultTeamSize: Yup.number() + .integer('Muss eine ganze Zahl sein.') + .min(1, 'Muss mindestens 1 sein.') + .required('Benötigt'), + mailingConfig: Yup.lazy((value: { enabled?: boolean } | undefined | null) => { + if (value?.enabled) { + return Yup.object().shape({ + enabled: Yup.boolean().defined(), + from: Yup.string() + .required('Benötigt') + .test({ + name: 'isValidFrom', + message: 'Muss eine kommaseparierte Liste mit "{email}" oder "{name} <{email}>" sein', + test: (value) => { + if (typeof value !== 'string') { + return false; + } + + const regexMail = /[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*/; + const regexName = /([\p{L}\p{N}",*-]|[^\S\r\n])+/; + const regex = new RegExp( + `^(${regexMail.source})|(${regexName.source} <${regexMail.source}>)$`, + 'u' + ); + const mails = value.split(',').map((m) => m.trim()); + + for (const mail of mails) { + if (!regex.test(mail)) { + return false; + } + } + + return true; + }, + }), + host: Yup.string().required('Benötigt'), + port: Yup.number() + .positive('Muss eine positive Zahl sein') + .integer('Muss eine ganze Zahl sein') + .required('Benötigt'), + auth: Yup.object().shape({ + user: Yup.string().required('Benötigt.'), + pass: Yup.string().required('Benötigt.'), + }), + }); + } else { + return Yup.mixed(); + } + }), +}); + +export interface FormState { + defaultTeamSize: string; + canTutorExcuseStudents: boolean; + mailingConfig: { + enabled: boolean; + from: string; + host: string; + port: string; + auth: { user: string; pass: string }; + }; +} + +export function getInitialValues(settings: IClientSettings): FormState { + const { mailingConfig, canTutorExcuseStudents, defaultTeamSize } = settings; + return { + canTutorExcuseStudents: canTutorExcuseStudents, + defaultTeamSize: `${defaultTeamSize}`, + mailingConfig: { + enabled: !!mailingConfig, + from: mailingConfig?.from ?? '', + host: mailingConfig?.host ?? '', + port: `${mailingConfig?.port ?? ''}`, + auth: { user: mailingConfig?.auth.user ?? '', pass: mailingConfig?.auth.pass ?? '' }, + }, + }; +} + +export function convertFormStateToDTO(values: FormState): IClientSettings { + const dto: IClientSettings = { + canTutorExcuseStudents: values.canTutorExcuseStudents, + defaultTeamSize: Number.parseInt(values.defaultTeamSize), + }; + + if (values.mailingConfig.enabled) { + const { from, host, port, auth } = values.mailingConfig; + dto.mailingConfig = { + from, + host, + port: Number.parseInt(port), + auth: { user: auth.user, pass: auth.pass }, + }; + } + + return dto; +} diff --git a/client/src/pages/settings/SettingsPage.tsx b/client/src/pages/settings/SettingsPage.tsx index 1363b6955..e11cb7cf0 100644 --- a/client/src/pages/settings/SettingsPage.tsx +++ b/client/src/pages/settings/SettingsPage.tsx @@ -1,215 +1,18 @@ -import { Box, Button, Typography } from '@material-ui/core'; -import { createStyles, makeStyles } from '@material-ui/core/styles'; -import { Formik, useFormikContext } from 'formik'; +import { Formik } from 'formik'; import React, { useCallback, useMemo } from 'react'; import { IClientSettings } from 'shared/model/Settings'; -import * as Yup from 'yup'; -import FormikCheckbox from '../../components/forms/components/FormikCheckbox'; -import FormikDebugDisplay from '../../components/forms/components/FormikDebugDisplay'; -import FormikTextField from '../../components/forms/components/FormikTextField'; -import GridDivider from '../../components/GridDivider'; -import SubmitButton from '../../components/loading/SubmitButton'; import Placeholder from '../../components/Placeholder'; -import { useDialog } from '../../hooks/DialogService'; import { setSettings } from '../../hooks/fetching/Settings'; import { useCustomSnackbar } from '../../hooks/snackbar/useCustomSnackbar'; import { useSettings } from '../../hooks/useSettings'; import { FormikSubmitCallback } from '../../types'; -import EMailSettings from './components/EMailSettings'; - -const useStyles = makeStyles((theme) => - createStyles({ - form: { display: 'flex', flexDirection: 'column' }, - unsavedChangesLabel: { marginLeft: theme.spacing(1) }, - input: { margin: theme.spacing(1, 0) }, - }) -); - -const validationSchema = Yup.object().shape({ - canTutorExcuseStudents: Yup.boolean().required('Benötigt'), - defaultTeamSize: Yup.number() - .integer('Muss eine ganze Zahl sein.') - .min(1, 'Muss mindestens 1 sein.') - .required('Benötigt'), - mailingConfig: Yup.lazy((value: { enabled?: boolean } | undefined | null) => { - if (value?.enabled) { - return Yup.object().shape({ - enabled: Yup.boolean().defined(), - from: Yup.string() - .required('Benötigt') - .test({ - name: 'isValidFrom', - message: 'Muss eine kommaseparierte Liste mit "{email}" oder "{name} <{email}>" sein', - test: (value) => { - if (typeof value !== 'string') { - return false; - } - - const regexMail = /[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*/; - const regexName = /([\p{L}\p{N}",*-]|[^\S\r\n])+/; - const regex = new RegExp( - `^(${regexMail.source})|(${regexName.source} <${regexMail.source}>)$`, - 'u' - ); - const mails = value.split(',').map((m) => m.trim()); - - for (const mail of mails) { - if (!regex.test(mail)) { - return false; - } - } - - return true; - }, - }), - host: Yup.string().required('Benötigt'), - port: Yup.number() - .positive('Muss eine positive Zahl sein') - .integer('Muss eine ganze Zahl sein') - .required('Benötigt'), - auth: Yup.object().shape({ - user: Yup.string().required('Benötigt.'), - pass: Yup.string().required('Benötigt.'), - }), - }); - } else { - return Yup.mixed(); - } - }), -}); - -interface FormState { - defaultTeamSize: string; - canTutorExcuseStudents: boolean; - mailingConfig: { - enabled: boolean; - from: string; - host: string; - port: string; - auth: { user: string; pass: string }; - }; -} - -function getInitialValues(settings: IClientSettings): FormState { - const { mailingConfig, canTutorExcuseStudents, defaultTeamSize } = settings; - return { - canTutorExcuseStudents: canTutorExcuseStudents, - defaultTeamSize: `${defaultTeamSize}`, - mailingConfig: { - enabled: !!mailingConfig, - from: mailingConfig?.from ?? '', - host: mailingConfig?.host ?? '', - port: `${mailingConfig?.port ?? ''}`, - auth: { user: mailingConfig?.auth.user ?? '', pass: mailingConfig?.auth.pass ?? '' }, - }, - }; -} - -function convertFormStateToDTO(values: FormState): IClientSettings { - const dto: IClientSettings = { - canTutorExcuseStudents: values.canTutorExcuseStudents, - defaultTeamSize: Number.parseInt(values.defaultTeamSize), - }; - - if (values.mailingConfig.enabled) { - const { from, host, port, auth } = values.mailingConfig; - dto.mailingConfig = { - from, - host, - port: Number.parseInt(port), - auth: { user: auth.user, pass: auth.pass }, - }; - } - - return dto; -} - -function SettingsPageForm(): JSX.Element { - const classes = useStyles(); - const { showConfirmationDialog } = useDialog(); - const { handleSubmit, isSubmitting, dirty, isValid, resetForm } = useFormikContext(); - - const handleFormReset = useCallback(async () => { - const result = await showConfirmationDialog({ - title: 'Einstellungen zurücksetzen?', - content: - 'Sollen die Einstellungen wirklich zurückgesetzt werden? Dies kann nicht rückgängig gemacht werden!', - acceptProps: { label: 'Zurücksetzen', deleteButton: true }, - cancelProps: { label: 'Abbrechen' }, - }); - - if (result) { - resetForm(); - } - }, [showConfirmationDialog, resetForm]); - - return ( -
- - - Einstellungen speichern - - - {dirty && ( - - Es gibt ungespeicherte Änderungen. - - )} - - - - - - Standardteamgröße - - - - - Anwesenheiten - - - - - E-Maileinstellungen - - - - - - ); -} +import SettingsPageForm from './components/SettingsPage.form'; +import { + convertFormStateToDTO, + FormState, + getInitialValues, + validationSchema, +} from './SettingsPage.helpers'; function SettingsPage(): JSX.Element { const { isLoadingSettings, settings, updateSettings } = useSettings(); diff --git a/client/src/pages/settings/components/SettingsPage.form.tsx b/client/src/pages/settings/components/SettingsPage.form.tsx new file mode 100644 index 000000000..ac6159c61 --- /dev/null +++ b/client/src/pages/settings/components/SettingsPage.form.tsx @@ -0,0 +1,109 @@ +import { Box, Button, Typography } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import { useFormikContext } from 'formik'; +import React, { useCallback } from 'react'; +import FormikCheckbox from '../../../components/forms/components/FormikCheckbox'; +import FormikDebugDisplay from '../../../components/forms/components/FormikDebugDisplay'; +import FormikTextField from '../../../components/forms/components/FormikTextField'; +import GridDivider from '../../../components/GridDivider'; +import SubmitButton from '../../../components/loading/SubmitButton'; +import { useDialog } from '../../../hooks/DialogService'; +import { FormState } from '../SettingsPage.helpers'; +import EMailSettings from './EMailSettings'; + +const useStyles = makeStyles((theme) => + createStyles({ + form: { display: 'flex', flexDirection: 'column' }, + unsavedChangesLabel: { marginLeft: theme.spacing(1) }, + input: { margin: theme.spacing(1, 0) }, + }) +); + +function SettingsPageForm(): JSX.Element { + const classes = useStyles(); + const { showConfirmationDialog } = useDialog(); + const { handleSubmit, isSubmitting, dirty, isValid, resetForm } = useFormikContext(); + + const handleFormReset = useCallback(async () => { + const result = await showConfirmationDialog({ + title: 'Einstellungen zurücksetzen?', + content: + 'Sollen die Einstellungen wirklich zurückgesetzt werden? Dies kann nicht rückgängig gemacht werden!', + acceptProps: { label: 'Zurücksetzen', deleteButton: true }, + cancelProps: { label: 'Abbrechen' }, + }); + + if (result) { + resetForm(); + } + }, [showConfirmationDialog, resetForm]); + + return ( +
+ + + Einstellungen speichern + + + {dirty && ( + + Es gibt ungespeicherte Änderungen. + + )} + + + + + + Standardteamgröße + + + + + Anwesenheiten + + + + + E-Maileinstellungen + + + + + + ); +} + +export default SettingsPageForm; From 8691349e3f0c7fa1ecf79fc44889ba5accecdcf4 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 19:51:16 +0200 Subject: [PATCH 48/62] Add mailing setting to customize the mail subject. --- client/src/routes/Routing.routes.ts | 7 ++++++- server/src/database/models/settings.model.ts | 7 +++++-- server/src/module/mail/mail.service.ts | 2 +- server/src/module/settings/model/MailingConfiguration.ts | 4 ++++ server/src/module/settings/settings.service.spec.ts | 3 +-- server/src/shared/model/Settings.ts | 1 + 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/client/src/routes/Routing.routes.ts b/client/src/routes/Routing.routes.ts index 5ac4654f5..1e8654cf5 100644 --- a/client/src/routes/Routing.routes.ts +++ b/client/src/routes/Routing.routes.ts @@ -269,6 +269,11 @@ const MANAGEMENT_ROUTES = { icon: SettingsIcon, roles: [Role.ADMIN], }), + DEBUG: new CustomRoute({ + path: parts('debug'), + title: 'DEBUG', + component: SettingsPage, + }), }; export const ROUTES = { @@ -277,5 +282,5 @@ export const ROUTES = { ...MANAGEMENT_ROUTES, }; -export const ROOT_REDIRECT_PATH = ROUTES.LOGIN; +export const ROOT_REDIRECT_PATH = ROUTES.DEBUG; export const PATH_REDIRECT_AFTER_LOGIN = ROUTES.DASHBOARD; diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index 6d35fffe3..93d7ac544 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -33,6 +33,9 @@ class MailingSettingsModel implements IMailingSettings { @prop({ required: true }) readonly from!: string; + @prop({ required: true }) + readonly subject!: string; + @prop({ required: true, type: MailingAuthModel }) readonly auth!: MailingAuthModel; @@ -41,8 +44,8 @@ class MailingSettingsModel implements IMailingSettings { } toDTO(): IMailingSettings { - const { host, port, from, auth } = this; - return { host, port, from, auth }; + const { host, port, from, auth, subject } = this; + return { host, port, from, auth, subject }; } } diff --git a/server/src/module/mail/mail.service.ts b/server/src/module/mail/mail.service.ts index 910d15a2b..99a0112bb 100644 --- a/server/src/module/mail/mail.service.ts +++ b/server/src/module/mail/mail.service.ts @@ -152,7 +152,7 @@ export class MailService { return await transport.sendMail({ from: options.from, to: user.email, - subject: 'TMS Credentials', // TODO: Make configurable + subject: options.subject, html: this.getTextOfMail(user), }); } catch (err) { diff --git a/server/src/module/settings/model/MailingConfiguration.ts b/server/src/module/settings/model/MailingConfiguration.ts index f26a0ae4f..c69266f94 100644 --- a/server/src/module/settings/model/MailingConfiguration.ts +++ b/server/src/module/settings/model/MailingConfiguration.ts @@ -24,6 +24,10 @@ export class MailingConfiguration implements IMailingSettings { @IsValidMailSender() readonly from!: string; + @IsString() + @IsNotEmpty() + readonly subject!: string; + @IsObject() @ValidateNested() @Type(() => MailingAuthConfiguration) diff --git a/server/src/module/settings/settings.service.spec.ts b/server/src/module/settings/settings.service.spec.ts index fd8514d88..c75ee275e 100644 --- a/server/src/module/settings/settings.service.spec.ts +++ b/server/src/module/settings/settings.service.spec.ts @@ -3,13 +3,12 @@ import { sanitizeObject } from '../../../test/helpers/test.helpers'; import { TestModule } from '../../../test/helpers/test.module'; import { MockedModel } from '../../../test/helpers/testdocument'; import { SETTINGS_DOCUMENTS } from '../../../test/mocks/documents.mock'; -import { SettingsModel } from '../../database/models/settings.model'; import { IClientSettings } from '../../shared/model/Settings'; import { ClientSettingsDTO } from './settings.dto'; import { SettingsService } from './settings.service'; interface AssertSettingsParams { - expected: MockedModel; + expected: MockedModel; actual: IClientSettings; } diff --git a/server/src/shared/model/Settings.ts b/server/src/shared/model/Settings.ts index 12a21c793..aecbb18d0 100644 --- a/server/src/shared/model/Settings.ts +++ b/server/src/shared/model/Settings.ts @@ -7,6 +7,7 @@ export interface IMailingSettings { from: string; host: string; port: number; + subject: string; auth: IMailingAuthConfiguration; } From 19dadcc7f35823b20606bebdadff048e799084cc Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 19:54:43 +0200 Subject: [PATCH 49/62] Add 'subject' field to EMailSettings form. --- .../pages/settings/SettingsPage.helpers.ts | 26 +++++++++++-------- .../settings/components/EMailSettings.tsx | 8 ++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/client/src/pages/settings/SettingsPage.helpers.ts b/client/src/pages/settings/SettingsPage.helpers.ts index f2fbb0ed6..3d5736154 100644 --- a/client/src/pages/settings/SettingsPage.helpers.ts +++ b/client/src/pages/settings/SettingsPage.helpers.ts @@ -11,6 +11,16 @@ export const validationSchema = Yup.object().shape({ if (value?.enabled) { return Yup.object().shape({ enabled: Yup.boolean().defined(), + host: Yup.string().required('Benötigt'), + subject: Yup.string().required('Benötigt'), + port: Yup.number() + .positive('Muss eine positive Zahl sein') + .integer('Muss eine ganze Zahl sein') + .required('Benötigt'), + auth: Yup.object().shape({ + user: Yup.string().required('Benötigt.'), + pass: Yup.string().required('Benötigt.'), + }), from: Yup.string() .required('Benötigt') .test({ @@ -38,15 +48,6 @@ export const validationSchema = Yup.object().shape({ return true; }, }), - host: Yup.string().required('Benötigt'), - port: Yup.number() - .positive('Muss eine positive Zahl sein') - .integer('Muss eine ganze Zahl sein') - .required('Benötigt'), - auth: Yup.object().shape({ - user: Yup.string().required('Benötigt.'), - pass: Yup.string().required('Benötigt.'), - }), }); } else { return Yup.mixed(); @@ -62,6 +63,7 @@ export interface FormState { from: string; host: string; port: string; + subject: string; auth: { user: string; pass: string }; }; } @@ -75,7 +77,8 @@ export function getInitialValues(settings: IClientSettings): FormState { enabled: !!mailingConfig, from: mailingConfig?.from ?? '', host: mailingConfig?.host ?? '', - port: `${mailingConfig?.port ?? ''}`, + port: `${mailingConfig?.port ?? '587'}`, + subject: mailingConfig?.subject ?? 'TMS Zugangsdaten', auth: { user: mailingConfig?.auth.user ?? '', pass: mailingConfig?.auth.pass ?? '' }, }, }; @@ -88,10 +91,11 @@ export function convertFormStateToDTO(values: FormState): IClientSettings { }; if (values.mailingConfig.enabled) { - const { from, host, port, auth } = values.mailingConfig; + const { from, host, port, auth, subject } = values.mailingConfig; dto.mailingConfig = { from, host, + subject, port: Number.parseInt(port), auth: { user: auth.user, pass: auth.pass }, }; diff --git a/client/src/pages/settings/components/EMailSettings.tsx b/client/src/pages/settings/components/EMailSettings.tsx index 168571074..8db1a8c9b 100644 --- a/client/src/pages/settings/components/EMailSettings.tsx +++ b/client/src/pages/settings/components/EMailSettings.tsx @@ -48,6 +48,14 @@ function EMailSettings(): JSX.Element { required={isEnabled} /> + + Date: Thu, 30 Jul 2020 21:41:24 +0200 Subject: [PATCH 50/62] Fix mailingConfig in SettingsModel set to {}. If no fields where given to the SettingsModel constructor an empty object was assigned to mailingConfig instead of undefined. --- server/src/database/models/settings.model.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/database/models/settings.model.ts b/server/src/database/models/settings.model.ts index 93d7ac544..24da45997 100644 --- a/server/src/database/models/settings.model.ts +++ b/server/src/database/models/settings.model.ts @@ -69,7 +69,10 @@ export class SettingsModel { fields?.defaultTeamSize ?? SettingsModel.internalDefaults.defaultTeamSize; this.canTutorExcuseStudents = fields?.canTutorExcuseStudents ?? SettingsModel.internalDefaults.canTutorExcuseStudents; - this.mailingConfig = new MailingSettingsModel(fields?.mailingConfig); + + this.mailingConfig = fields?.mailingConfig + ? new MailingSettingsModel(fields.mailingConfig) + : undefined; } toDTO(): IClientSettings { From 1ed29b3f02f1926d59672b5cfd110f742746dfd0 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 30 Jul 2020 22:25:23 +0200 Subject: [PATCH 51/62] Fix wrong creation of transport options. --- server/src/module/mail/mail.service.ts | 16 +++++++++++----- server/src/module/settings/settings.service.ts | 7 +++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/server/src/module/mail/mail.service.ts b/server/src/module/mail/mail.service.ts index 99a0112bb..6b4e259c1 100644 --- a/server/src/module/mail/mail.service.ts +++ b/server/src/module/mail/mail.service.ts @@ -5,8 +5,8 @@ import { SentMessageInfo } from 'nodemailer/lib/smtp-transport'; import { UserDocument } from '../../database/models/user.model'; import { VALID_EMAIL_REGEX } from '../../helpers/validators/nodemailer.validator'; import { FailedMail, MailingStatus } from '../../shared/model/Mail'; +import { IMailingSettings } from '../../shared/model/Settings'; import { getNameOfEntity } from '../../shared/util/helpers'; -import { MailingConfiguration } from '../settings/model/MailingConfiguration'; import { SettingsService } from '../settings/settings.service'; import { TemplateService } from '../template/template.service'; import { UserService } from '../user/user.service'; @@ -18,7 +18,7 @@ class MailingError { interface SendMailParams { user: UserDocument; transport: Mail; - options: MailingConfiguration; + options: IMailingSettings; } @Injectable() @@ -153,7 +153,7 @@ export class MailService { from: options.from, to: user.email, subject: options.subject, - html: this.getTextOfMail(user), + text: this.getTextOfMail(user), }); } catch (err) { throw new MailingError(user.id, 'SEND_MAIL_FAILED', err); @@ -177,9 +177,15 @@ export class MailService { * @param options Options to create the transport with. * @returns A nodemail SMTPTransport instance created with the given options. */ - private createSMTPTransport(options: MailingConfiguration): Mail { + private createSMTPTransport(options: IMailingSettings): Mail { // TODO: Add testingMode with ethereal! - return nodemailer.createTransport(options); + return nodemailer.createTransport({ + host: options.host, + port: options.port, + auth: { user: options.auth.user, pass: options.auth.pass }, + logger: true, + debug: true, + }); } /** diff --git a/server/src/module/settings/settings.service.ts b/server/src/module/settings/settings.service.ts index 9ef5a9fad..d6cf85439 100644 --- a/server/src/module/settings/settings.service.ts +++ b/server/src/module/settings/settings.service.ts @@ -3,8 +3,7 @@ import { ReturnModelType } from '@typegoose/typegoose'; import { InjectModel } from 'nestjs-typegoose'; import { SettingsDocument, SettingsModel } from '../../database/models/settings.model'; import { StartUpException } from '../../exceptions/StartUpException'; -import { IClientSettings } from '../../shared/model/Settings'; -import { MailingConfiguration } from './model/MailingConfiguration'; +import { IClientSettings, IMailingSettings } from '../../shared/model/Settings'; import { ClientSettingsDTO } from './settings.dto'; import { StaticSettings } from './settings.static'; @@ -47,9 +46,9 @@ export class SettingsService extends StaticSettings implements OnModuleInit { /** * @returns MailingConfiguration saved in the DB or `undefined` if none are saved. */ - async getMailingOptions(): Promise { + async getMailingOptions(): Promise { const document = await this.getSettingsDocument(); - return document.mailingConfig; + return document.mailingConfig?.toDTO(); } /** From 58a64ad071829c23c80eecb48efb59b09f78e3e1 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 09:57:21 +0200 Subject: [PATCH 52/62] Add helpertexts to SettingsPage textfields. This also changes the order of the inputs on the email settings. --- .../settings/components/EMailSettings.tsx | 26 ++++++++++++------- .../settings/components/SettingsPage.form.tsx | 1 + 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/client/src/pages/settings/components/EMailSettings.tsx b/client/src/pages/settings/components/EMailSettings.tsx index 8db1a8c9b..5b0c50209 100644 --- a/client/src/pages/settings/components/EMailSettings.tsx +++ b/client/src/pages/settings/components/EMailSettings.tsx @@ -29,6 +29,7 @@ function EMailSettings(): JSX.Element { className={classes.input} disabled={!isEnabled} required={isEnabled} + helperText='Hostaddresse des SMTP-EMailservers.' /> - - ); diff --git a/client/src/pages/settings/components/SettingsPage.form.tsx b/client/src/pages/settings/components/SettingsPage.form.tsx index ac6159c61..d370ad6d3 100644 --- a/client/src/pages/settings/components/SettingsPage.form.tsx +++ b/client/src/pages/settings/components/SettingsPage.form.tsx @@ -82,6 +82,7 @@ function SettingsPageForm(): JSX.Element { Date: Fri, 31 Jul 2020 09:57:44 +0200 Subject: [PATCH 53/62] Fix form validation allowing a list for the 'from' email setting. --- client/src/pages/settings/SettingsPage.helpers.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/client/src/pages/settings/SettingsPage.helpers.ts b/client/src/pages/settings/SettingsPage.helpers.ts index 3d5736154..5c525e38b 100644 --- a/client/src/pages/settings/SettingsPage.helpers.ts +++ b/client/src/pages/settings/SettingsPage.helpers.ts @@ -25,7 +25,7 @@ export const validationSchema = Yup.object().shape({ .required('Benötigt') .test({ name: 'isValidFrom', - message: 'Muss eine kommaseparierte Liste mit "{email}" oder "{name} <{email}>" sein', + message: 'Muss entweder in der Form "{email}" oder "{name} <{email}>" sein', test: (value) => { if (typeof value !== 'string') { return false; @@ -37,15 +37,7 @@ export const validationSchema = Yup.object().shape({ `^(${regexMail.source})|(${regexName.source} <${regexMail.source}>)$`, 'u' ); - const mails = value.split(',').map((m) => m.trim()); - - for (const mail of mails) { - if (!regex.test(mail)) { - return false; - } - } - - return true; + return regex.test(value); }, }), }); From 39e8445409c0e1a142b1d8a12fdc4996abafc406 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 14:28:54 +0200 Subject: [PATCH 54/62] Add direct support for secondary text to ListItemMenu items. --- client/src/components/list-item-menu/ListItemMenu.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/components/list-item-menu/ListItemMenu.tsx b/client/src/components/list-item-menu/ListItemMenu.tsx index b46dec613..51573df63 100644 --- a/client/src/components/list-item-menu/ListItemMenu.tsx +++ b/client/src/components/list-item-menu/ListItemMenu.tsx @@ -16,9 +16,10 @@ type UsedProps = export interface ListItem extends MenuItemProps<'button'> { primary: string; + secondary?: string; onClick: MouseEventHandler; Icon: ComponentType; - listItemTextProps?: ListItemTextProps; + listItemTextProps?: Omit; iconProps?: SvgIconProps; tooltip?: string; } @@ -31,6 +32,7 @@ export interface ListItemMenuProps extends Omit { function generateListItem({ Icon, primary, + secondary, onClick, iconProps, listItemTextProps, @@ -44,7 +46,7 @@ function generateListItem({ - + ); From 3a9c1e7bb02e8f8995996c116dbab37f5bf4c136 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 14:29:09 +0200 Subject: [PATCH 55/62] Add helper function to determine if server has mailing configuration set. --- client/src/hooks/useSettings.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/hooks/useSettings.tsx b/client/src/hooks/useSettings.tsx index e45199504..ced147bcb 100644 --- a/client/src/hooks/useSettings.tsx +++ b/client/src/hooks/useSettings.tsx @@ -12,6 +12,7 @@ interface ContextType { isLoadingSettings: boolean; canStudentBeExcused: () => boolean; updateSettings: () => Promise; + isMailingActive: () => boolean; } const DEFAULT_SETTINGS: IClientSettings = { defaultTeamSize: 1, canTutorExcuseStudents: false }; @@ -21,6 +22,7 @@ const SettingsContext = React.createContext({ isLoadingSettings: false, canStudentBeExcused: throwContextNotInitialized('SettingsContext'), updateSettings: throwContextNotInitialized('SettingsContext'), + isMailingActive: throwContextNotInitialized('SettingsContext'), }); export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element { @@ -48,6 +50,8 @@ export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element return !!userData && userData.roles.includes(Role.ADMIN); }, [userData, value]); + const isMailingActive = useCallback(() => !!value?.mailingConfig, [value?.mailingConfig]); + return ( {children} From ce32db2e64d40055c48bdc840516b76c16cf4b59 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 14:33:47 +0200 Subject: [PATCH 56/62] Add disabling of send credentials buttons for users if: - Mailing is disabled (this also disables the button to send the credentials to all users) OR - User has no email address OR - User has no temporary password. The reason is displayed to the user as secondary text except for the first one. --- .../pages/usermanagement/UserManagement.tsx | 38 ++++++++++++++----- .../components/UserTableRow.tsx | 9 ++++- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/client/src/pages/usermanagement/UserManagement.tsx b/client/src/pages/usermanagement/UserManagement.tsx index 389400d88..036569e78 100644 --- a/client/src/pages/usermanagement/UserManagement.tsx +++ b/client/src/pages/usermanagement/UserManagement.tsx @@ -7,7 +7,7 @@ import { } from 'mdi-material-ui'; import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { MailingStatus } from 'shared/model/Mail'; +import { FailedMail, MailingStatus } from 'shared/model/Mail'; import { Role } from 'shared/model/Role'; import { ICreateUserDTO, IUser, IUserDTO } from 'shared/model/User'; import { getNameOfEntity } from 'shared/util/helpers'; @@ -28,6 +28,7 @@ import { setTemporaryPassword, } from '../../hooks/fetching/User'; import { useCustomSnackbar } from '../../hooks/snackbar/useCustomSnackbar'; +import { useSettings } from '../../hooks/useSettings'; import { Tutorial } from '../../model/Tutorial'; import { ROUTES } from '../../routes/Routing.routes'; import { saveBlob } from '../../util/helperFunctions'; @@ -81,14 +82,26 @@ function convertFormStateToUserDTO( }; } +function convertFailedMailsInfoIntoList(failedMailsInfo: FailedMail[], users: IUser[]): string[] { + return failedMailsInfo.map((info) => { + const user = users.find((u) => u.id === info.userId); + const name = user ? getNameOfEntity(user) : 'USER_NOT_FOUND'; + const reason = info.reason; + + return `${name}: ${reason}`; + }); +} + function UserManagement(): JSX.Element { const classes = useStyles(); + const dialog = useDialog(); + const { isMailingActive } = useSettings(); + const { enqueueSnackbar, closeSnackbar, enqueueSnackbarWithList } = useCustomSnackbar(); + const [isLoading, setIsLoading] = useState(false); const [isSendingCredentials, setSendingCredentials] = useState(false); const [users, setUsers] = useState([]); const [tutorials, setTutorials] = useState([]); - const { enqueueSnackbar, closeSnackbar, enqueueSnackbarWithList } = useCustomSnackbar(); - const dialog = useDialog(); useEffect(() => { setIsLoading(true); @@ -260,17 +273,13 @@ function UserManagement(): JSX.Element { if (failedMailsInfo.length === 0) { enqueueSnackbar('Zugangsdaten wurden erfolgreich verschickt.', { variant: 'success' }); } else { - const failedNames: string[] = failedMailsInfo.map((info) => { - const user = users.find((u) => u.id === info.userId); - - return user ? getNameOfEntity(user) : 'NOT_FOUND'; - }); + const convertedInfo: string[] = convertFailedMailsInfoIntoList(failedMailsInfo, users); enqueueSnackbarWithList({ title: 'Nicht zugestellte Zugangsdaten', textBeforeList: 'Die Zugangsdaten konnten nicht an folgende Nutzer/innen zugestellt werden:', - items: failedNames, + items: convertedInfo, isOpen: true, }); } @@ -305,7 +314,11 @@ function UserManagement(): JSX.Element { if (status.failedMailsInfo.length === 0) { enqueueSnackbar('Zugangsdaten erfolgreich verschickt.', { variant: 'success' }); } else { - enqueueSnackbar('Zugangsdaten verschicken fehlgeschlagen.', { variant: 'error' }); + const name = getNameOfEntity(user, { firstNameFirst: true }); + enqueueSnackbar( + `Zugangsdaten verschicken an ${name} fehlgeschlagen: ${status.failedMailsInfo[0].reason}`, + { variant: 'error' } + ); } } catch { enqueueSnackbar('Zugangsdaten verschicken fehlgeschlagen.', { variant: 'error' }); @@ -334,6 +347,10 @@ function UserManagement(): JSX.Element { isSubmitting={isSendingCredentials} startIcon={} onClick={handleSendCredentials} + disabled={!isMailingActive()} + tooltipText={ + !isMailingActive() ? 'E-Mails sind serverseitig nicht konfiguriert.' : undefined + } > Zugangsdaten verschicken @@ -366,6 +383,7 @@ function UserManagement(): JSX.Element { onEditUserClicked={handleEditUser} onDeleteUserClicked={handleDeleteUser} onSendCredentialsClicked={sendCredentialsToSingleUser} + disableSendCredentials={!isMailingActive()} /> )} /> diff --git a/client/src/pages/usermanagement/components/UserTableRow.tsx b/client/src/pages/usermanagement/components/UserTableRow.tsx index 54e54c5bf..493fdd219 100644 --- a/client/src/pages/usermanagement/components/UserTableRow.tsx +++ b/client/src/pages/usermanagement/components/UserTableRow.tsx @@ -18,6 +18,7 @@ const useStyles = makeStyles((theme: Theme) => interface Props extends PaperTableRowProps { user: IUser; + disableSendCredentials?: boolean; onEditUserClicked: (user: IUser) => void; onDeleteUserClicked: (user: IUser) => void; onSendCredentialsClicked: (user: IUser) => void; @@ -29,6 +30,7 @@ function getRolesAsString(roles: Role[]): string { function UserTableRow({ user, + disableSendCredentials, onEditUserClicked, onDeleteUserClicked, onSendCredentialsClicked, @@ -48,9 +50,14 @@ function UserTableRow({ additionalItems={[ { primary: 'Zugangsdaten schicken', + secondary: + (disableSendCredentials && 'Vom Server nicht unterstützt.') || + (!user.email && 'Nutzer/in hat keine E-Mail') || + (!user.temporaryPassword && 'Nutzer/in hat kein temporäres Passwort.') || + undefined, onClick: () => onSendCredentialsClicked(user), Icon: MailIcon, - disabled: !user.email, + disabled: disableSendCredentials || !user.email || !user.temporaryPassword, }, ]} /> From 748856af9f5036c12a8070dd5b6e535f492dbae9 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 14:36:32 +0200 Subject: [PATCH 57/62] Remove debug route. --- client/src/routes/Routing.routes.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/src/routes/Routing.routes.ts b/client/src/routes/Routing.routes.ts index 1e8654cf5..5ac4654f5 100644 --- a/client/src/routes/Routing.routes.ts +++ b/client/src/routes/Routing.routes.ts @@ -269,11 +269,6 @@ const MANAGEMENT_ROUTES = { icon: SettingsIcon, roles: [Role.ADMIN], }), - DEBUG: new CustomRoute({ - path: parts('debug'), - title: 'DEBUG', - component: SettingsPage, - }), }; export const ROUTES = { @@ -282,5 +277,5 @@ export const ROUTES = { ...MANAGEMENT_ROUTES, }; -export const ROOT_REDIRECT_PATH = ROUTES.DEBUG; +export const ROOT_REDIRECT_PATH = ROUTES.LOGIN; export const PATH_REDIRECT_AFTER_LOGIN = ROUTES.DASHBOARD; From 9915ea1c01fe7d6f56e0c5f35b7733fbba28b985 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 15:37:10 +0200 Subject: [PATCH 58/62] Add more validation to the configuration on server start. --- .../settings/model/DatabaseConfiguration.ts | 18 ++++++++---- server/src/module/settings/settings.static.ts | 29 ++++++++++++------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/server/src/module/settings/model/DatabaseConfiguration.ts b/server/src/module/settings/model/DatabaseConfiguration.ts index 151d67d5c..7b91f19ea 100644 --- a/server/src/module/settings/model/DatabaseConfiguration.ts +++ b/server/src/module/settings/model/DatabaseConfiguration.ts @@ -1,15 +1,23 @@ import { IsNumber, IsObject, IsOptional, IsString, Min } from 'class-validator'; import { ConnectionOptions } from 'mongoose'; +export enum DatabaseConfigurationValidationGroup { + ALL = 'all', + FILE = 'file', +} + export class DatabaseConfiguration { - @IsString() + @IsString({ always: true }) readonly databaseURL!: string; - @IsNumber() - @Min(0) + @IsNumber({}, { always: true }) + @Min(0, { always: true }) readonly maxRetries!: number; - @IsOptional() - @IsObject() + @IsString({ groups: [DatabaseConfigurationValidationGroup.ALL] }) + readonly secret!: string; + + @IsOptional({ always: true }) + @IsObject({ always: true }) readonly config?: Omit; } diff --git a/server/src/module/settings/settings.static.ts b/server/src/module/settings/settings.static.ts index e4fb0b9b3..869cd7498 100644 --- a/server/src/module/settings/settings.static.ts +++ b/server/src/module/settings/settings.static.ts @@ -6,20 +6,20 @@ import path from 'path'; import YAML from 'yaml'; import { StartUpException } from '../../exceptions/StartUpException'; import { ApplicationConfiguration } from './model/ApplicationConfiguration'; -import { DatabaseConfiguration } from './model/DatabaseConfiguration'; +import { + DatabaseConfiguration, + DatabaseConfigurationValidationGroup, +} from './model/DatabaseConfiguration'; import { EnvironmentConfig, ENV_VARIABLE_NAMES } from './model/EnvironmentConfig'; -type DatabaseConfig = DatabaseConfiguration & { secret: string }; - export class StaticSettings { private static service: StaticSettings = new StaticSettings(); private static readonly API_PREFIX = 'api'; private static readonly STATIC_FOLDER = 'app'; - // TODO: Redo these properties to have a more readable 'loading flow'. protected readonly config: ApplicationConfiguration; - private readonly databaseConfig: Readonly; + private readonly databaseConfig: DatabaseConfiguration; private readonly envConfig: EnvironmentConfig; protected readonly logger = new Logger(StaticSettings.name); @@ -54,7 +54,7 @@ export class StaticSettings { /** * @returns Configuration for the database. */ - getDatabaseConfiguration(): DatabaseConfig { + getDatabaseConfiguration(): DatabaseConfiguration { return this.databaseConfig; } @@ -127,10 +127,9 @@ export class StaticSettings { * * @returns Configuration options for the database. */ - private loadDatabaseConfig(): DatabaseConfig { + private loadDatabaseConfig(): DatabaseConfiguration { const configFromFile: DatabaseConfiguration = this.config.database; - - return { + const config: DatabaseConfiguration = plainToClass(DatabaseConfiguration, { databaseURL: configFromFile.databaseURL, maxRetries: configFromFile.maxRetries, secret: this.envConfig.secret, @@ -139,7 +138,13 @@ export class StaticSettings { user: this.envConfig.mongoDbUser, pass: this.envConfig.mongoDbPassword, }, - }; + }); + + this.assertConfigNoErrors( + validateSync(config, { groups: [DatabaseConfigurationValidationGroup.ALL] }) + ); + + return config; } /** @@ -283,7 +288,9 @@ export class StaticSettings { const configString = YAML.parse(fileContent); const config = plainToClass(ApplicationConfiguration, configString); - this.assertConfigNoErrors(validateSync(config)); + this.assertConfigNoErrors( + validateSync(config, { groups: [DatabaseConfigurationValidationGroup.FILE] }) + ); this.logger.log(`Configuration loaded for "${environment}" environment`); return config; From 9145d04cd8cd00cc84211ec94d57e4c376f36f13 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 15:55:55 +0200 Subject: [PATCH 59/62] Update yarn.lock --- yarn.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8b80727e7..79394f3ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14362,11 +14362,6 @@ uuid@^3.0.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - v8-compile-cache@^2.0.3: version "2.1.1" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" From ac5c95d4923462dbf0c19e8c9447fa3d06429551 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 31 Jul 2020 16:38:26 +0200 Subject: [PATCH 60/62] Resolve todos. --- .../components/FormikMarkdownTextfield.tsx | 19 +++++++++++++++++-- server/src/module/mail/mail.service.ts | 1 - 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/client/src/components/forms/components/FormikMarkdownTextfield.tsx b/client/src/components/forms/components/FormikMarkdownTextfield.tsx index 3c8feea16..657ec3edb 100644 --- a/client/src/components/forms/components/FormikMarkdownTextfield.tsx +++ b/client/src/components/forms/components/FormikMarkdownTextfield.tsx @@ -41,7 +41,19 @@ const useStyles = makeStyles((theme: Theme) => }) ); -function FormikMarkdownTextfield({ name, className, ...other }: FormikTextFieldProps): JSX.Element { +interface Props { + /** Disables the submit of the form by pressing Ctrl + Enter */ + disableSendOnCtrlEnter?: boolean; +} + +type FormikMarkdownTextfieldProps = Props & FormikTextFieldProps; + +function FormikMarkdownTextfield({ + name, + className, + disableSendOnCtrlEnter, + ...other +}: FormikMarkdownTextfieldProps): JSX.Element { const classes = useStyles(); const { handleSubmit, dirty } = useFormikContext(); const [{ value }] = useField(name); @@ -55,7 +67,10 @@ function FormikMarkdownTextfield({ name, className, ...other }: FormikTextFieldP }, [value]); const handleKeyDown: React.KeyboardEventHandler = (event) => { - // FIXME: Does this need to be in here? Can it use the useKeyboardShortcut() hook or better - can this be handled by the parent component? + if (disableSendOnCtrlEnter) { + return; + } + if (event.ctrlKey && event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); diff --git a/server/src/module/mail/mail.service.ts b/server/src/module/mail/mail.service.ts index 6b4e259c1..b99b40cc2 100644 --- a/server/src/module/mail/mail.service.ts +++ b/server/src/module/mail/mail.service.ts @@ -178,7 +178,6 @@ export class MailService { * @returns A nodemail SMTPTransport instance created with the given options. */ private createSMTPTransport(options: IMailingSettings): Mail { - // TODO: Add testingMode with ethereal! return nodemailer.createTransport({ host: options.host, port: options.port, From 0e2330c6fe56960b62eb064b5088331e200fa732 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 1 Aug 2020 21:00:08 +0200 Subject: [PATCH 61/62] Fix wrong dependency array. --- client/src/hooks/useSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/hooks/useSettings.tsx b/client/src/hooks/useSettings.tsx index ced147bcb..a4063644a 100644 --- a/client/src/hooks/useSettings.tsx +++ b/client/src/hooks/useSettings.tsx @@ -50,7 +50,7 @@ export function SettingsProvider({ children }: RequireChildrenProp): JSX.Element return !!userData && userData.roles.includes(Role.ADMIN); }, [userData, value]); - const isMailingActive = useCallback(() => !!value?.mailingConfig, [value?.mailingConfig]); + const isMailingActive = useCallback(() => !!value?.mailingConfig, [value]); return ( Date: Sat, 1 Aug 2020 21:00:21 +0200 Subject: [PATCH 62/62] Fix style issues with the SplitButton. --- client/src/components/SplitButton.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/client/src/components/SplitButton.tsx b/client/src/components/SplitButton.tsx index e38ea49b7..1742c9fc8 100644 --- a/client/src/components/SplitButton.tsx +++ b/client/src/components/SplitButton.tsx @@ -1,6 +1,7 @@ import { Button, ButtonGroup, + ButtonGroupProps, ButtonProps, ClickAwayListener, Grow, @@ -8,11 +9,24 @@ import { MenuList, Paper, Popper, - ButtonGroupProps, } from '@material-ui/core'; +import { createStyles, fade, makeStyles } from '@material-ui/core/styles'; import { MenuDown as ArrowDropDownIcon } from 'mdi-material-ui'; import React from 'react'; +const useStyles = makeStyles((theme) => + createStyles({ + button: { + '&:hover': { + background: fade(theme.palette.primary.main, 0.8), + }, + '&:not(:last-child)': { + borderRightColor: theme.palette.getContrastText(theme.palette.primary.main), + }, + }, + }) +); + interface ButtonOption { label: string; disabled?: boolean; @@ -29,6 +43,7 @@ function SplitButton({ options, variant, color, ...props }: Props): JSX.Element const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); const [selectedIndex, setSelectedIndex] = React.useState(0); + const classes = useStyles(); const { ButtonProps: buttonProps } = options[selectedIndex]; @@ -60,6 +75,7 @@ function SplitButton({ options, variant, color, ...props }: Props): JSX.Element ref={anchorRef} aria-label='split button' {...props} + classes={{ grouped: classes.button }} >