Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ DB_SCHEMA=public
DB_SSL=
REDIS_HOST=localhost
REDIS_PORT=6379
JWT_CLIENT_TOKEN_SECRET=SuperSecret1
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ services:
REDIS_HOST: redis
REDIS_PORT: 6379
SSL_REDIRECTION_ENABLED: 0
JWT_CLIENT_TOKEN_SECRET: SuperSecret1
volumes:
- .:/opt/app
ports:
Expand Down
787 changes: 785 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json --runInBand --detectOpenHandles --forceExit"
},
"dependencies": {
"@nestjs/common": "^7.5.1",
"@nestjs/core": "^7.5.1",
"@nestjs/platform-express": "^7.5.1",
"@nestjs/platform-socket.io": "^7.6.5",
"@nestjs/swagger": "^4.7.8",
"@nestjs/typeorm": "^7.1.5",
"@nestjs/websockets": "^7.6.5",
"@types/jsonwebtoken": "^8.5.0",
"class-transformer": "^0.3.1",
"class-validator": "^0.12.2",
Expand All @@ -42,6 +44,7 @@
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.6.3",
"socket.io": "^2.3.0",
"swagger-ui-express": "^4.1.5",
"typeorm": "0.2.25",
"uuid": "^8.3.2"
Expand All @@ -55,6 +58,8 @@
"@types/helmet": "^4.0.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/socket.io": "^2.1.12",
"@types/socket.io-client": "^1.4.34",
"@types/supertest": "^2.0.10",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.9.1",
Expand All @@ -72,6 +77,7 @@
"jest-date-mock": "^1.0.8",
"jest-extended": "^0.11.5",
"prettier": "^2.1.2",
"socket.io-client": "^2.3.0",
"supertest": "^6.0.0",
"ts-jest": "^26.4.3",
"ts-loader": "^8.0.8",
Expand Down
3 changes: 3 additions & 0 deletions setupTests.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
import 'reflect-metadata';
import { Logger } from '@nestjs/common';

Logger.overrideLogger([]);
11 changes: 7 additions & 4 deletions src/ApplicationModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConnectionOptions } from 'typeorm';
import { JwtDecodeMiddleware } from './infrastructure/security/jwt/JwtDecodeMiddleware';
import { ApiModule } from './api/ApiModule';
import { HttpApiModule } from './api/http/HttpApiModule';
import { VotingDomainModule } from './domain/VotingDomainModule';
import { InfrastructureModule } from './infrastructure/InfrastructureModule'; // eslint-disable-line import/order
import { InfrastructureModule } from './infrastructure/InfrastructureModule';
import { WebSocketDispatcherApiModule } from './api/ws/voting/dispatcher/WebSocketDispatcherApiModule';
import { WebSocketReceiverApiModule } from './api/ws/voting/receiver/WebSocketReceiverApiModule'; // eslint-disable-line import/order, max-len

// eslint-disable-next-line @typescript-eslint/no-var-requires
const ormConfig = require('../ormconfig');

const votingDomain = VotingDomainModule.forRoot([InfrastructureModule]);
const votingDomain = VotingDomainModule.forRoot([InfrastructureModule, WebSocketDispatcherApiModule]);

@Module({
imports: [
Expand All @@ -20,7 +22,8 @@ const votingDomain = VotingDomainModule.forRoot([InfrastructureModule]);
};
},
}),
ApiModule.forRoot([votingDomain]),
HttpApiModule.forRoot([votingDomain]),
WebSocketReceiverApiModule.forRoot([votingDomain]),
],
})
export class ApplicationModule {
Expand Down
36 changes: 36 additions & 0 deletions src/api/AbstractExceptionFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { BaseExceptionFilter } from '@nestjs/core';
import { ArgumentsHost } from '@nestjs/common';

export type ExceptionMap<Out = Error, In = Error> = Record<string, (error: In) => Out>;

export abstract class AbstractExceptionFilter extends BaseExceptionFilter {
/**
* Catches and processes the exception
*
* @param exception - Thrown error
* @param host - Argument host
*/
public catch(exception: Error, host: ArgumentsHost): void {
super.catch(this.transformException(exception), host);
}

protected abstract get exceptionMap(): ExceptionMap;

/**
* Transforms internal exceptions to related api exceptions
*
* @param exception - Thrown error
* @returns The transformed error
*/
private transformException(exception: Error): Error {
const httpExceptionFactoryMethodKey = Object.keys(this.exceptionMap).find(
(className) => exception.constructor.name === className,
);

if (!httpExceptionFactoryMethodKey) {
return exception;
}

return this.exceptionMap[httpExceptionFactoryMethodKey](exception);
}
}
31 changes: 0 additions & 31 deletions src/api/ApiModule.ts

This file was deleted.

31 changes: 31 additions & 0 deletions src/api/http/HttpApiModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DynamicModule, Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { ImportModule } from '../../util/ImportModule';
import { ApiExceptionFilter } from './rest/ApiExceptionFilter';
import { HealthCheckController } from './rest/healthcheck/controller/HealthCheckController';
import { SessionController } from './rest/voting/session/controller/SessionController';
import { CreateSessionRequestResponseFactory } from './rest/voting/session/factory/CreateSessionRequestResponseFactory';
import { CreateTopicRequestResponseFactory } from './rest/voting/session/factory/CreateTopicRequestResponseFactory';
import { CreateParticipantRequestResponseFactory } from './rest/voting/session/factory/CreateParticipantRequestResponseFactory';
import { ExternalIdComposer } from './rest/voting/session/factory/ExternalIdComposer';
import { TopicController } from './rest/voting/session/controller/TopicController';
import { ParticipantController } from './rest/voting/session/controller/ParticipantController';

@Module({
controllers: [HealthCheckController, SessionController, TopicController, ParticipantController],
providers: [
{ provide: APP_FILTER, useClass: ApiExceptionFilter },
CreateSessionRequestResponseFactory,
CreateTopicRequestResponseFactory,
CreateParticipantRequestResponseFactory,
ExternalIdComposer,
],
})
export class HttpApiModule {
public static forRoot(imports: ImportModule[]): DynamicModule {
return {
module: HttpApiModule,
imports,
};
}
}
14 changes: 8 additions & 6 deletions src/api/http/rest/ApiExceptionFilter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { ArgumentsHost, HttpServer, Logger } from '@nestjs/common';
import { createMock } from '@golevelup/nestjs-testing';
import { TokenInvalidError } from '../../../infrastructure/security/jwt/TokenInvalidError';
import { TokenNotFoundError } from '../../../infrastructure/security/jwt/TokenNotFoundError';
import { SessionNotFoundException } from '../../../domain';
import { ParticipantForMandateNotExistingException } from '../../../domain/exception/ParticipantForMandateNotExistingException';
import { ParticipantAlreadyExistsException } from '../../../domain/exception/ParticipantAlreadyExistsException';
import { ParticipantDuplicatedException } from '../../../domain/exception/ParticipantDuplicatedException';
import { TopicAlreadyExistsException } from '../../../domain/exception/TopicAlreadyExistsException';
import { TopicDuplicatedException } from '../../../domain/exception/TopicDuplicatedException';
import {
ParticipantAlreadyExistsException,
ParticipantDuplicatedException,
ParticipantForMandateNotExistingException,
SessionNotFoundException,
TopicAlreadyExistsException,
TopicDuplicatedException,
} from '../../../domain';
import { ApiExceptionFilter } from './ApiExceptionFilter';

describe('ApiExceptionFilter', () => {
Expand Down
113 changes: 42 additions & 71 deletions src/api/http/rest/ApiExceptionFilter.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,51 @@
import { BaseExceptionFilter } from '@nestjs/core';
import {
ArgumentsHost,
BadRequestException,
Catch,
HttpException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { BadRequestException, Catch, HttpException, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { TokenInvalidError } from '../../../infrastructure/security/jwt/TokenInvalidError';
import { TokenNotFoundError } from '../../../infrastructure/security/jwt/TokenNotFoundError';
import { SessionNotFoundException } from '../../../domain';
import { ParticipantForMandateNotExistingException } from '../../../domain/exception/ParticipantForMandateNotExistingException';
import { ParticipantAlreadyExistsException } from '../../../domain/exception/ParticipantAlreadyExistsException';
import { ParticipantDuplicatedException } from '../../../domain/exception/ParticipantDuplicatedException';
import { TopicAlreadyExistsException } from '../../../domain/exception/TopicAlreadyExistsException';
import { TopicDuplicatedException } from '../../../domain/exception/TopicDuplicatedException';
import {
ParticipantAlreadyExistsException,
ParticipantDuplicatedException,
ParticipantForMandateNotExistingException,
SessionNotFoundException,
TopicAlreadyExistsException,
TopicDuplicatedException,
} from '../../../domain';
import { AbstractExceptionFilter, ExceptionMap } from '../../AbstractExceptionFilter';
import { ExternalIdComposer } from './voting/session/factory/ExternalIdComposer';

@Catch()
export class ApiExceptionFilter extends BaseExceptionFilter {
export class ApiExceptionFilter extends AbstractExceptionFilter {
private externalIdComposer = new ExternalIdComposer();

private exceptionMap: Record<string, (error: Error) => HttpException> = {
[TokenInvalidError.name]: (e: Error) => new UnauthorizedException(e.message),
[TokenNotFoundError.name]: (e: Error) => new UnauthorizedException(e.message),
[SessionNotFoundException.name]: (e: Error) => new NotFoundException(e.message),
[ParticipantForMandateNotExistingException.name]: (e: ParticipantForMandateNotExistingException) =>
new BadRequestException(
`Cannot create mandate for participant with id ${this.externalIdComposer.decompose(
e.id,
e.clientId,
)}. Participant does not exist`,
),
[ParticipantAlreadyExistsException.name]: (e: ParticipantAlreadyExistsException) =>
new BadRequestException(
`Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`,
),
[ParticipantDuplicatedException.name]: (e: ParticipantDuplicatedException) =>
new BadRequestException(
`Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`,
),
[TopicAlreadyExistsException.name]: (e: TopicAlreadyExistsException) =>
new BadRequestException(
`Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`,
),
[TopicDuplicatedException.name]: (e: TopicDuplicatedException) =>
new BadRequestException(
`Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`,
),
};

/**
* Catches and processes the exception
*
* @param exception - Thrown error
* @param host - Argument host
*/
public catch(exception: Error, host: ArgumentsHost): void {
super.catch(this.transformException(exception), host);
}

/**
* Transforms internal exceptions to related api exceptions
*
* @param exception - Thrown error
* @returns The transformed error
*/
private transformException(exception: Error): Error {
const httpExceptionFactoryMethodKey = Object.keys(this.exceptionMap).find(
(className) => exception.constructor.name === className,
);

if (!httpExceptionFactoryMethodKey) {
return exception;
}

return this.exceptionMap[httpExceptionFactoryMethodKey](exception);
protected get exceptionMap(): ExceptionMap<HttpException> {
return {
[TokenInvalidError.name]: (e: Error): HttpException => new UnauthorizedException(e.message),
[TokenNotFoundError.name]: (e: Error): HttpException => new UnauthorizedException(e.message),
[SessionNotFoundException.name]: (e: Error): HttpException => new NotFoundException(e.message),
[ParticipantForMandateNotExistingException.name]: (
e: ParticipantForMandateNotExistingException,
): HttpException =>
new BadRequestException(
`Cannot create mandate for participant with id ${this.externalIdComposer.decompose(
e.id,
e.clientId,
)}. Participant does not exist`,
),
[ParticipantAlreadyExistsException.name]: (e: ParticipantAlreadyExistsException): HttpException =>
new BadRequestException(
`Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`,
),
[ParticipantDuplicatedException.name]: (e: ParticipantDuplicatedException): HttpException =>
new BadRequestException(
`Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`,
),
[TopicAlreadyExistsException.name]: (e: TopicAlreadyExistsException): HttpException =>
new BadRequestException(
`Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`,
),
[TopicDuplicatedException.name]: (e: TopicDuplicatedException): HttpException =>
new BadRequestException(
`Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`,
),
};
}
}
Loading