chore: first commit

This commit is contained in:
mahyargdz
2025-06-24 11:26:06 +03:30
commit ccbf743ecb
186 changed files with 22258 additions and 0 deletions
@@ -0,0 +1,49 @@
import { HttpException, HttpStatus } from "@nestjs/common";
export class MailServerException extends HttpException {
constructor(message: string, status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR) {
super(message, status);
}
}
export class UserNotFoundException extends MailServerException {
constructor(userId: string) {
super(`User with ID '${userId}' not found`, HttpStatus.NOT_FOUND);
}
}
export class MailboxNotFoundException extends MailServerException {
constructor(mailboxId: string) {
super(`Mailbox with ID '${mailboxId}' not found`, HttpStatus.NOT_FOUND);
}
}
export class MessageNotFoundException extends MailServerException {
constructor(messageId: number) {
super(`Message with ID '${messageId}' not found`, HttpStatus.NOT_FOUND);
}
}
export class UserAlreadyExistsException extends MailServerException {
constructor(username: string) {
super(`User '${username}' already exists`, HttpStatus.CONFLICT);
}
}
export class AddressAlreadyExistsException extends MailServerException {
constructor(address: string) {
super(`Address '${address}' already exists`, HttpStatus.CONFLICT);
}
}
export class MailServerConnectionException extends MailServerException {
constructor(details?: string) {
super(`Mail server connection failed${details ? `: ${details}` : ""}`, HttpStatus.SERVICE_UNAVAILABLE);
}
}
export class InvalidMailServerResponseException extends MailServerException {
constructor() {
super("Invalid response from mail server", HttpStatus.BAD_GATEWAY);
}
}
+47
View File
@@ -0,0 +1,47 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
import { FastifyReply, FastifyRequest } from "fastify";
import { ErrorResponse } from "../../common/interfaces/IError-response";
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const reply = ctx.getResponse<FastifyReply>();
const request = ctx.getRequest<FastifyRequest>();
const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionResponse = exception.getResponse();
const message = this.extractMessage(exceptionResponse);
const response: ErrorResponse = {
statusCode: status,
success: false,
error: {
message,
path: request.url,
},
};
reply.status(status).send(response);
}
private extractMessage(response: string | Record<string, any>): string[] {
if (typeof response === "string") {
return [response];
}
if (typeof response === "object" && response !== null) {
if ("message" in response) {
const message = response.message;
return Array.isArray(message) ? message : [message];
}
if ("error" in response) {
return [response.error];
}
}
return ["An unexpected error occurred"];
}
}
@@ -0,0 +1,41 @@
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { FastifyReply, FastifyRequest } from "fastify";
import { MailServerException } from "../exceptions/mail-server.exceptions";
@Catch(MailServerException)
export class MailServerExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(MailServerExceptionFilter.name);
catch(exception: MailServerException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<FastifyReply>();
const request = ctx.getRequest<FastifyRequest>();
const status = exception.getStatus();
const message = exception.message;
// Log the error for monitoring
this.logger.error({
message: "Mail server exception occurred",
url: request.url,
method: request.method,
status,
error: message,
userId: (request.params as any)?.userId || (request.params as any)?.id,
userAgent: request.headers["user-agent"],
ip: request.ip,
});
// Return a consistent error response
response.status(status).send({
success: false,
error: {
code: exception.constructor.name,
message,
timestamp: new Date().toISOString(),
path: request.url,
},
});
}
}
@@ -0,0 +1,42 @@
import { BadRequestException, CallHandler, ExecutionContext, ForbiddenException, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { FastifyRequest } from "fastify";
import { Observable } from "rxjs";
import { BusinessMessage } from "../../common/enums/message.enum";
import { Business } from "../../modules/businesses/entities/business.entity";
import { BusinessesService } from "../../modules/businesses/services/businesses.service";
declare module "fastify" {
interface FastifyRequest {
business?: Business;
}
}
@Injectable()
export class BusinessInterceptor implements NestInterceptor {
private readonly headerName = "x-business-id";
private readonly logger = new Logger(BusinessInterceptor.name);
constructor(private readonly businessesService: BusinessesService) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const businessId = request.headers[this.headerName.toLocaleLowerCase()] as string;
if (businessId) {
try {
const business = await this.businessesService.getBusinessByDanakSubscriptionId(businessId);
request.business = business;
} catch (error) {
this.logger.error(error);
throw new BadRequestException(BusinessMessage.NOT_FOUND);
}
} else {
throw new ForbiddenException(BusinessMessage.BUSINESS_ID_REQUIRED);
}
return next.handle();
}
}
@@ -0,0 +1,79 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { Observable } from "rxjs";
import { catchError, tap } from "rxjs/operators";
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const { method, url, body, query, params, headers } = request;
const startTime = Date.now();
const requestId = this.generateRequestId();
// Log request
this.logger.log({
message: "Incoming request",
requestId,
method,
url,
query,
params,
userAgent: headers["user-agent"],
ip: request.ip,
// Don't log sensitive data
body: this.sanitizeRequestBody(body),
});
return next.handle().pipe(
tap((data) => {
const duration = Date.now() - startTime;
this.logger.log({
message: "Request completed",
requestId,
method,
url,
statusCode: response.statusCode,
duration: `${duration}ms`,
responseSize: JSON.stringify(data).length,
});
}),
catchError((error) => {
const duration = Date.now() - startTime;
this.logger.error({
message: "Request failed",
requestId,
method,
url,
statusCode: response.statusCode,
duration: `${duration}ms`,
error: error.message,
stack: error.stack,
});
throw error;
}),
);
}
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private sanitizeRequestBody(body: any): any {
if (!body || typeof body !== "object") return body;
const sanitized = { ...body };
const sensitiveFields = ["password", "token", "authorization", "secret"];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = "***REDACTED***";
}
}
return sanitized;
}
}
@@ -0,0 +1,84 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
interface RequestMetrics {
method: string;
url: string;
statusCode: number;
responseTime: number;
timestamp: string;
userAgent?: string;
ip?: string;
}
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
private readonly logger = new Logger(MetricsInterceptor.name);
private static metrics: Map<string, any> = new Map();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const startTime = Date.now();
return next.handle().pipe(
tap(() => {
const responseTime = Date.now() - startTime;
const metrics: RequestMetrics = {
method: request.method,
url: request.url,
statusCode: response.statusCode,
responseTime,
timestamp: new Date().toISOString(),
userAgent: request.headers["user-agent"],
ip: request.ip,
};
this.collectMetrics(metrics);
// Log slow requests
if (responseTime > 1000) {
this.logger.warn(`Slow request detected: ${request.method} ${request.url} - ${responseTime}ms`);
}
}),
);
}
private collectMetrics(metrics: RequestMetrics): void {
const key = `${metrics.method}:${metrics.url}`;
if (!MetricsInterceptor.metrics.has(key)) {
MetricsInterceptor.metrics.set(key, {
count: 0,
totalResponseTime: 0,
avgResponseTime: 0,
maxResponseTime: 0,
minResponseTime: Infinity,
errorCount: 0,
});
}
const existing = MetricsInterceptor.metrics.get(key);
existing.count++;
existing.totalResponseTime += metrics.responseTime;
existing.avgResponseTime = existing.totalResponseTime / existing.count;
existing.maxResponseTime = Math.max(existing.maxResponseTime, metrics.responseTime);
existing.minResponseTime = Math.min(existing.minResponseTime, metrics.responseTime);
if (metrics.statusCode >= 400) {
existing.errorCount++;
}
MetricsInterceptor.metrics.set(key, existing);
}
static getMetrics(): Map<string, any> {
return MetricsInterceptor.metrics;
}
static resetMetrics(): void {
MetricsInterceptor.metrics.clear();
}
}
+97
View File
@@ -0,0 +1,97 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { FastifyRequest } from "fastify";
import { Observable, map } from "rxjs";
import { IPageFormat, PaginatedResponse, PaginationQuery } from "../../common/interfaces/IPagination";
@Injectable()
export class PaginationInterceptor implements NestInterceptor {
private readonly logger = new Logger(PaginationInterceptor.name);
private readonly DEFAULT_PAGE = 1;
private readonly DEFAULT_LIMIT = 10;
private readonly MAX_LIMIT = 100;
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const { page, limit } = this.extractPaginationParams(request.query as PaginationQuery);
const { className, handlerName } = this.getContextInfo(context);
return next.handle().pipe(
map((data: PaginatedResponse) => {
if (!this.isPaginatedResponse(data)) {
return data;
}
this.logger.log(`Paginating response from ${className}.${handlerName}`);
const { count, paginate, ...response } = data;
const pager = this.formatPage(page, limit, count, request);
return {
pager,
...response,
};
}),
);
}
//******* Extract Pagination Params *******//
private extractPaginationParams(query: PaginationQuery): { page: number; limit: number } {
const page = this.validateNumber(query.page, this.DEFAULT_PAGE);
const limit = this.validateNumber(query.limit, this.DEFAULT_LIMIT);
return {
page: Math.max(1, page),
limit: Math.min(Math.max(1, limit), this.MAX_LIMIT),
};
}
//******* Validate Number *******//
private validateNumber(value: string | undefined, defaultValue: number): number {
const parsed = parseInt(value || "", 10);
return isNaN(parsed) ? defaultValue : parsed;
}
//******* Get Context Info *******//
private getContextInfo(context: ExecutionContext): { className: string; handlerName: string } {
return {
className: context.getClass().name,
handlerName: context.getHandler().name,
};
}
//******* Check if Response is Paginated *******//
private isPaginatedResponse(data: PaginatedResponse): data is PaginatedResponse {
return data && (data.paginate || typeof data.count === "number");
}
//******* Format Page *******//
private formatPage(page: number, limit: number, totalItems: number | undefined, request: FastifyRequest): IPageFormat {
const count = totalItems || 0;
const totalPages = Math.ceil(count / limit);
const prevPage = page > 1 ? page - 1 : false;
const nextPage = page < totalPages ? page + 1 : false;
return {
page,
limit,
totalItems: count,
totalPages,
prevPage: this.generatePageLink(prevPage, limit, request),
nextPage: this.generatePageLink(nextPage, limit, request),
};
}
//******* Generate Page Link *******//
private generatePageLink(page: number | boolean, limit: number, request: FastifyRequest): string | boolean {
if (!page) return false;
const { protocol, hostname, url } = request;
const baseUrl = url.split("?")[0];
const queryParams = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
return `${protocol}://${hostname}${baseUrl}?${queryParams.toString()}`;
}
}
+30
View File
@@ -0,0 +1,30 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { FastifyReply } from "fastify";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
//
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
const ctx = context.switchToHttp().getResponse<FastifyReply>();
const statusCode = ctx.statusCode;
return next.handle().pipe(
map((data) => {
if (data && data.data !== undefined) {
return {
statusCode,
success: true,
data: data.data,
};
}
return {
statusCode,
success: true,
data,
};
}),
);
}
}
+30
View File
@@ -0,0 +1,30 @@
import { Injectable, Logger, NestMiddleware } from "@nestjs/common";
import { FastifyReply, FastifyRequest } from "fastify";
@Injectable()
export class HTTPLogger implements NestMiddleware {
private readonly logger = new Logger(HTTPLogger.name);
use(req: FastifyRequest, rep: FastifyReply["raw"], next: () => void): void {
const startAt = process.hrtime();
const { method, originalUrl } = req;
const ip = req.ip || req.socket.remoteAddress || "-";
const userAgent = req.headers["user-agent"] || "-";
const referer = req.headers.referer || "-";
rep.on("finish", () => {
const { statusCode } = rep;
const contentLength = rep.getHeader("content-length") || 0;
const dif = process.hrtime(startAt);
const responseTime = dif[0] * 1e3 + dif[1] * 1e-6;
const timestamp = new Date().toISOString();
// Apache-like format: IP - - [timestamp] "METHOD URL" STATUS SIZE "REFERER" "USER-AGENT" RESPONSE_TIME
this.logger.log(
`${ip} - - [${timestamp}] "${method} ${originalUrl}" ${statusCode} ${contentLength} "${referer}" "${userAgent}" ${responseTime.toFixed(2)}ms`,
);
});
next();
}
}