error-handling #18
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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