added basic error handling for backend
This commit is contained in:
parent
61dcf21730
commit
666360fa3a
|
@ -8,6 +8,8 @@ import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { SessionInitService } from './modules/session/services';
|
import { SessionInitService } from './modules/session/services';
|
||||||
|
import { HttpExceptionFilter } from './shared/filters';
|
||||||
|
import { ErrorHandlingInterceptor } from './shared/interceptors';
|
||||||
|
|
||||||
async function setupSwagger(app: INestApplication): Promise<void> {
|
async function setupSwagger(app: INestApplication): Promise<void> {
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
|
@ -50,12 +52,22 @@ async function setupClassValidator(app: INestApplication): Promise<void> {
|
||||||
app.useGlobalPipes(new ValidationPipe());
|
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> {
|
async function bootstrap(): Promise<void> {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
await setupCookieParser(app);
|
await setupCookieParser(app);
|
||||||
await setupSwagger(app);
|
await setupSwagger(app);
|
||||||
await setupPrefix(app);
|
await setupPrefix(app);
|
||||||
|
await setupGlobalFilters(app);
|
||||||
|
await setupGlobalInterceptors(app);
|
||||||
await setupClassValidator(app);
|
await setupClassValidator(app);
|
||||||
await setupSessions(app);
|
await setupSessions(app);
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
ConflictException,
|
|
||||||
ForbiddenException,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
Injectable,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { UserCredentials } from 'src/entities';
|
import { UserCredentials } from 'src/entities';
|
||||||
import { SessionService } from 'src/modules/session/services/session.service';
|
import { SessionService } from 'src/modules/session/services/session.service';
|
||||||
import { EncryptionService, SuccessDto } from 'src/shared';
|
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 { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
||||||
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
||||||
|
@ -34,7 +33,7 @@ export class AuthService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new ConflictException('User already exists');
|
throw new ConflictException('USER_ALREADY_EXISTS');
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordHashed = await EncryptionService.hashData(
|
const passwordHashed = await EncryptionService.hashData(
|
||||||
|
@ -63,14 +62,11 @@ export class AuthService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ConflictException) {
|
if (error instanceof ConflictException) {
|
||||||
throw new ConflictException(
|
throw error;
|
||||||
'User already exists. Please try to login instead.'
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(
|
throw new InternalServerErrorException('SIGNUP_ERROR', {
|
||||||
'Error while signing up',
|
cause: error,
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +79,7 @@ export class AuthService {
|
||||||
const user = await this.userCredentialsRepository.findUserByEmail(email);
|
const user = await this.userCredentialsRepository.findUserByEmail(email);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ForbiddenException('Access Denied');
|
throw new ForbiddenException('INVALID_CREDENTIALS');
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatch = await EncryptionService.compareHash(
|
const passwordMatch = await EncryptionService.compareHash(
|
||||||
|
@ -92,60 +88,61 @@ export class AuthService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
throw new ForbiddenException('Access Denied');
|
throw new ForbiddenException('INVALID_CREDENTIALS');
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ForbiddenException) {
|
if (error instanceof ForbiddenException) {
|
||||||
throw new ForbiddenException(
|
throw error;
|
||||||
'E-Mail address or password is incorrect. Please try again.'
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(
|
throw new InternalServerErrorException('VALIDATION_ERROR', {
|
||||||
'Error while validating user credentials',
|
cause: error,
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signout(sessionId: string): Promise<{ success: boolean }> {
|
public async signout(sessionId: string): Promise<SuccessDto> {
|
||||||
try {
|
try {
|
||||||
this.sessionService.deleteSessionBySessionId(sessionId);
|
await this.sessionService.deleteSessionBySessionId(sessionId);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
throw new InternalServerErrorException('SIGNOUT_ERROR', {
|
||||||
'Fehler beim Logout',
|
message:
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
'An error occurred during the sign out process. Please try again later.',
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkAuthStatus(
|
public async checkAuthStatus(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
userAgend: string
|
userAgent: string
|
||||||
): Promise<SuccessDto> {
|
): Promise<SuccessDto> {
|
||||||
try {
|
try {
|
||||||
const session =
|
const session =
|
||||||
await this.sessionService.findSessionBySessionId(sessionId);
|
await this.sessionService.findSessionBySessionId(sessionId);
|
||||||
|
|
||||||
if (!session) {
|
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;
|
.userAgent;
|
||||||
|
|
||||||
if (userAgendFromSession !== userAgend) {
|
if (userAgentFromSession !== userAgent) {
|
||||||
throw new ForbiddenException('User-Agent does not match');
|
throw new ForbiddenException('USER_AGENT_MISMATCH');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
if (error instanceof ForbiddenException) {
|
||||||
'Error while checking auth status',
|
throw error;
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
} else {
|
||||||
);
|
throw new InternalServerErrorException('AUTH_STATUS_CHECK_ERROR', {
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ConfigService } from '@nestjs/config';
|
||||||
import { EmailVerification } from 'src/entities';
|
import { EmailVerification } from 'src/entities';
|
||||||
import { SessionService } from 'src/modules/session/services/session.service';
|
import { SessionService } from 'src/modules/session/services/session.service';
|
||||||
import { UriEncoderService } from 'src/shared';
|
import { UriEncoderService } from 'src/shared';
|
||||||
|
import { InternalServerErrorException } from 'src/shared/exceptions';
|
||||||
|
|
||||||
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
||||||
import { EmailVerifyRepository } from '../repositories';
|
import { EmailVerifyRepository } from '../repositories';
|
||||||
|
@ -19,58 +20,77 @@ export class EmailVerificationService {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async generateEmailVerificationToken(userId: string): Promise<string> {
|
public async generateEmailVerificationToken(userId: string): Promise<string> {
|
||||||
const verificationToken = await this.createVerificationToken();
|
try {
|
||||||
|
const verificationToken = await this.createVerificationToken();
|
||||||
|
const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
// TODO Check users local time zone and set expiration time accordingly
|
await this.emailVerifyRepository.createEmailVerification(
|
||||||
const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
verificationToken,
|
||||||
|
expiration,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
this.emailVerifyRepository.createEmailVerification(
|
return verificationToken;
|
||||||
verificationToken,
|
} catch (error) {
|
||||||
expiration,
|
throw new InternalServerErrorException(
|
||||||
userId
|
'EMAIL_VERIFICATION_TOKEN_GENERATION_ERROR',
|
||||||
);
|
{
|
||||||
|
message:
|
||||||
return verificationToken;
|
'An error occurred while generating the email verification token.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifyEmail(tokenToVerify: string): Promise<boolean> {
|
public async verifyEmail(tokenToVerify: string): Promise<boolean> {
|
||||||
const isTokenVerified =
|
try {
|
||||||
await this.emailVerifyRepository.findEmailVerificationByToken(
|
|
||||||
tokenToVerify
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTokenVerified) {
|
|
||||||
const emailVerification =
|
const emailVerification =
|
||||||
|
await this.emailVerifyRepository.findEmailVerificationByToken(
|
||||||
|
tokenToVerify
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!emailVerification) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedVerification =
|
||||||
await this.deleteEmailVerificationToken(tokenToVerify);
|
await this.deleteEmailVerificationToken(tokenToVerify);
|
||||||
|
|
||||||
if (emailVerification && emailVerification.user) {
|
if (deletedVerification && deletedVerification.user) {
|
||||||
const isStatusUpdated =
|
const isStatusUpdated =
|
||||||
await this.userDataRepository.updateEmailVerificationStatus(
|
await this.userDataRepository.updateEmailVerificationStatus(
|
||||||
emailVerification.user.id
|
deletedVerification.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
return isStatusUpdated;
|
return isStatusUpdated;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
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> {
|
public async isEmailVerified(sessionID: string): Promise<boolean> {
|
||||||
const userId = await this.sessionService.getUserIdBySessionId(sessionID);
|
try {
|
||||||
|
const userId = await this.sessionService.getUserIdBySessionId(sessionID);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVerified =
|
||||||
|
await this.userDataRepository.isEmailConfirmedByUserId(userId);
|
||||||
|
|
||||||
|
return isVerified;
|
||||||
|
} catch (error) {
|
||||||
|
throw new InternalServerErrorException('EMAIL_VERIFICATION_CHECK_ERROR', {
|
||||||
|
message:
|
||||||
|
'An error occurred while checking the email verification status.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isVerfiied =
|
|
||||||
await this.userDataRepository.isEmailConfirmedByUserId(userId);
|
|
||||||
|
|
||||||
if (isVerfiied) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createVerificationToken(): Promise<string> {
|
private async createVerificationToken(): Promise<string> {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './conflict.exception';
|
||||||
|
export * from './forbidden.exception';
|
||||||
|
export * from './internal-server-error.exception';
|
||||||
|
export * from './not-found.exception';
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './http-exception.filter';
|
|
@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './error-handling.interceptor';
|
Loading…
Reference in New Issue