error-handling #18

Merged
igorpropisnov merged 2 commits from feature/error-handling into main 2024-09-08 18:01:18 +02:00
13 changed files with 266 additions and 74 deletions

View File

@ -8,6 +8,8 @@ import * as cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { SessionInitService } from './modules/session/services';
import { HttpExceptionFilter } from './shared/filters';
import { ErrorHandlingInterceptor } from './shared/interceptors';
async function setupSwagger(app: INestApplication): Promise<void> {
const config = new DocumentBuilder()
@ -50,12 +52,22 @@ async function setupClassValidator(app: INestApplication): Promise<void> {
app.useGlobalPipes(new ValidationPipe());
}
async function setupGlobalFilters(app: INestApplication): Promise<void> {
app.useGlobalFilters(new HttpExceptionFilter());
}
async function setupGlobalInterceptors(app: INestApplication): Promise<void> {
app.useGlobalInterceptors(new ErrorHandlingInterceptor());
}
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
await setupCookieParser(app);
await setupSwagger(app);
await setupPrefix(app);
await setupGlobalFilters(app);
await setupGlobalInterceptors(app);
await setupClassValidator(app);
await setupSessions(app);
await app.listen(3000);

View File

@ -1,13 +1,12 @@
import {
ConflictException,
ForbiddenException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { UserCredentials } from 'src/entities';
import { SessionService } from 'src/modules/session/services/session.service';
import { EncryptionService, SuccessDto } from 'src/shared';
import {
ConflictException,
ForbiddenException,
InternalServerErrorException,
} from 'src/shared/exceptions';
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
@ -34,7 +33,7 @@ export class AuthService {
);
if (existingUser) {
throw new ConflictException('User already exists');
throw new ConflictException('USER_ALREADY_EXISTS');
}
const passwordHashed = await EncryptionService.hashData(
@ -63,14 +62,11 @@ export class AuthService {
};
} catch (error) {
if (error instanceof ConflictException) {
throw new ConflictException(
'User already exists. Please try to login instead.'
);
throw error;
} else {
throw new HttpException(
'Error while signing up',
HttpStatus.INTERNAL_SERVER_ERROR
);
throw new InternalServerErrorException('SIGNUP_ERROR', {
cause: error,
});
}
}
}
@ -83,7 +79,7 @@ export class AuthService {
const user = await this.userCredentialsRepository.findUserByEmail(email);
if (!user) {
throw new ForbiddenException('Access Denied');
throw new ForbiddenException('INVALID_CREDENTIALS');
}
const passwordMatch = await EncryptionService.compareHash(
@ -92,60 +88,61 @@ export class AuthService {
);
if (!passwordMatch) {
throw new ForbiddenException('Access Denied');
throw new ForbiddenException('INVALID_CREDENTIALS');
}
return user;
} catch (error) {
if (error instanceof ForbiddenException) {
throw new ForbiddenException(
'E-Mail address or password is incorrect. Please try again.'
);
throw error;
} else {
throw new HttpException(
'Error while validating user credentials',
HttpStatus.INTERNAL_SERVER_ERROR
);
throw new InternalServerErrorException('VALIDATION_ERROR', {
cause: error,
});
}
}
}
public async signout(sessionId: string): Promise<{ success: boolean }> {
public async signout(sessionId: string): Promise<SuccessDto> {
try {
this.sessionService.deleteSessionBySessionId(sessionId);
await this.sessionService.deleteSessionBySessionId(sessionId);
return { success: true };
} catch (error) {
throw new HttpException(
'Fehler beim Logout',
HttpStatus.INTERNAL_SERVER_ERROR
);
throw new InternalServerErrorException('SIGNOUT_ERROR', {
message:
'An error occurred during the sign out process. Please try again later.',
});
}
}
public async checkAuthStatus(
sessionId: string,
userAgend: string
userAgent: string
): Promise<SuccessDto> {
try {
const session =
await this.sessionService.findSessionBySessionId(sessionId);
if (!session) {
throw new ForbiddenException('Session not found');
throw new ForbiddenException('SESSION_NOT_FOUND');
}
const userAgendFromSession = JSON.parse(session.json).passport.user
const userAgentFromSession = JSON.parse(session.json).passport.user
.userAgent;
if (userAgendFromSession !== userAgend) {
throw new ForbiddenException('User-Agent does not match');
if (userAgentFromSession !== userAgent) {
throw new ForbiddenException('USER_AGENT_MISMATCH');
}
return { success: true };
} catch (error) {
throw new HttpException(
'Error while checking auth status',
HttpStatus.INTERNAL_SERVER_ERROR
);
if (error instanceof ForbiddenException) {
throw error;
} else {
throw new InternalServerErrorException('AUTH_STATUS_CHECK_ERROR', {
cause: error,
});
}
}
}

View File

@ -1,10 +1,10 @@
import { randomBytes } from 'crypto';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EmailVerification } from 'src/entities';
import { SessionService } from 'src/modules/session/services/session.service';
import { UriEncoderService } from 'src/shared';
import { InternalServerErrorException } from 'src/shared/exceptions';
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
import { EmailVerifyRepository } from '../repositories';
@ -14,63 +14,81 @@ export class EmailVerificationService {
public constructor(
private readonly emailVerifyRepository: EmailVerifyRepository,
private readonly userDataRepository: UserDataRepository,
private readonly sessionService: SessionService,
private readonly configService: ConfigService
private readonly sessionService: SessionService
) {}
public async generateEmailVerificationToken(userId: string): Promise<string> {
try {
const verificationToken = await this.createVerificationToken();
// TODO Check users local time zone and set expiration time accordingly
const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000);
this.emailVerifyRepository.createEmailVerification(
await this.emailVerifyRepository.createEmailVerification(
verificationToken,
expiration,
userId
);
return verificationToken;
} catch (error) {
throw new InternalServerErrorException(
'EMAIL_VERIFICATION_TOKEN_GENERATION_ERROR',
{
message:
'An error occurred while generating the email verification token.',
}
);
}
}
public async verifyEmail(tokenToVerify: string): Promise<boolean> {
const isTokenVerified =
try {
const emailVerification =
await this.emailVerifyRepository.findEmailVerificationByToken(
tokenToVerify
);
if (isTokenVerified) {
const emailVerification =
if (!emailVerification) {
return false;
}
const deletedVerification =
await this.deleteEmailVerificationToken(tokenToVerify);
if (emailVerification && emailVerification.user) {
if (deletedVerification && deletedVerification.user) {
const isStatusUpdated =
await this.userDataRepository.updateEmailVerificationStatus(
emailVerification.user.id
deletedVerification.user.id
);
return isStatusUpdated;
}
}
return false;
} catch (error) {
throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', {
message: 'An error occurred while verifying the email.',
});
}
}
public async isEmailVerified(sessionID: string): Promise<boolean> {
try {
const userId = await this.sessionService.getUserIdBySessionId(sessionID);
if (!userId) {
return false;
}
const isVerfiied =
const isVerified =
await this.userDataRepository.isEmailConfirmedByUserId(userId);
if (isVerfiied) {
return true;
return isVerified;
} catch (error) {
throw new InternalServerErrorException('EMAIL_VERIFICATION_CHECK_ERROR', {
message:
'An error occurred while checking the email verification status.',
});
}
return false;
}
private async createVerificationToken(): Promise<string> {

View File

@ -0,0 +1,11 @@
export class BaseException extends Error {
public constructor(
public readonly message: string,
public readonly status: number,
public readonly error: string,
public readonly details?: unknown
) {
super(message);
this.name = this.constructor.name;
}
}

View File

@ -0,0 +1,9 @@
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class ConflictException extends BaseException {
public constructor(errorCode: string, details?: unknown) {
super('Conflict Error', HttpStatus.CONFLICT, errorCode, details);
}
}

View File

@ -0,0 +1,9 @@
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class ForbiddenException extends BaseException {
public constructor(errorCode: string, details?: unknown) {
super('Forbidden Error', HttpStatus.FORBIDDEN, errorCode, details);
}
}

View File

@ -0,0 +1,4 @@
export * from './conflict.exception';
export * from './forbidden.exception';
export * from './internal-server-error.exception';
export * from './not-found.exception';

View File

@ -0,0 +1,14 @@
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class InternalServerErrorException extends BaseException {
public constructor(errorCode: string, details?: unknown) {
super(
'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
errorCode,
details
);
}
}

View File

@ -0,0 +1,9 @@
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class NotFoundException extends BaseException {
public constructor(errorCode: string, details?: unknown) {
super('Not Found Error', HttpStatus.NOT_FOUND, errorCode, details);
}
}

View File

@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Response } from 'express';
import { BaseException } from '../exceptions/base.exception';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger: Logger = new Logger(HttpExceptionFilter.name);
public catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
let message: string = 'Internal server error';
let error: string = 'INTERNAL_SERVER_ERROR';
let details: unknown = undefined;
if (exception instanceof BaseException) {
status = exception.status;
message = exception.message;
error = exception.error;
details = exception.details;
} else if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
}
// Logging
this.logger.error(`${error}: ${message}`, exception);
// TODO: Error reporting (mock implementation)
this.reportError(error, message, details);
response.status(status).json({
status: status,
message,
error,
details: this.sanitizeErrorDetails(details),
timestamp: new Date().toISOString(),
});
}
private sanitizeErrorDetails(details: any): any {
if (details && typeof details === 'object') {
const sanitized = { ...details };
// TODO: Remove sensitive data
// delete sanitized.password;
// delete sanitized.creditCard;
return sanitized;
}
return details;
}
private reportError(errorCode: string, message: string, details: any): void {
console.log(`Error reported: ${errorCode} - ${message}`);
// TODO: Implement error reporting (i. e. Sentry)
// Example Sentry.captureException(new Error(`${errorCode}: ${message}`), { extra: details });
}
}

View File

@ -0,0 +1 @@
export * from './http-exception.filter';

View File

@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
HttpException,
InternalServerErrorException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { BaseException } from '../exceptions/base.exception';
@Injectable()
export class ErrorHandlingInterceptor implements NestInterceptor {
public intercept(
context: ExecutionContext,
next: CallHandler
): Observable<any> {
return next.handle().pipe(
catchError((error) => {
if (error instanceof BaseException) {
throw error;
} else if (error instanceof HttpException) {
throw error;
} else {
throw new InternalServerErrorException({
message: 'An unexpected error occurred',
errorCode: 'UNEXPECTED_ERROR',
details: { originalError: error.message },
});
}
})
);
}
}

View File

@ -0,0 +1 @@
export * from './error-handling.interceptor';