fix: bug in monitoring service

This commit is contained in:
mahyargdz
2025-07-26 16:57:56 +03:30
parent c6defaa1d8
commit 00847a9449
8 changed files with 90 additions and 115 deletions
+5 -1
View File
@@ -20,5 +20,9 @@ export function getSwaggerDocument(app: NestFastifyApplication) {
.build(); .build();
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig); const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup("api-docs", app, swaggerDocument); SwaggerModule.setup("api-docs", app, swaggerDocument, {
swaggerOptions: {
persistAuthorization: true,
},
});
} }
@@ -1,16 +1,16 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsOptional, IsString } from "class-validator"; import { IsOptional, IsString } from "class-validator";
export class Setup2FADto { export class Setup2FADto {
@ApiPropertyOptional({ description: "TOTP issuer name", example: "DMail Service" })
@IsOptional() @IsOptional()
@IsString() @IsString()
@ApiPropertyOptional({ description: "TOTP issuer name", example: "DMail Service" })
issuer?: string; issuer?: string;
@ApiPropertyOptional({ description: "Generate fresh secret", default: false })
@IsOptional() @IsOptional()
@IsBoolean() @IsString()
fresh?: boolean; @ApiPropertyOptional({ description: "TOTP label", example: "DMail Service" })
label?: string;
} }
export class Enable2FADto { export class Enable2FADto {
+7
View File
@@ -54,6 +54,7 @@ export class AuthController {
return this.authService.changePassword(userId, changePasswordDto); return this.authService.changePassword(userId, changePasswordDto);
} }
@AuthGuards()
@Post("setup") @Post("setup")
@ApiOperation({ summary: "Setup TOTP 2FA for user" }) @ApiOperation({ summary: "Setup TOTP 2FA for user" })
@ApiResponse({ status: 200, description: "2FA setup initiated successfully" }) @ApiResponse({ status: 200, description: "2FA setup initiated successfully" })
@@ -62,6 +63,7 @@ export class AuthController {
return this.twoFactorService.setup2FA(wildduckUserId, setup2FADto); return this.twoFactorService.setup2FA(wildduckUserId, setup2FADto);
} }
@AuthGuards()
@Post("enable") @Post("enable")
@ApiOperation({ summary: "Enable TOTP 2FA after verification" }) @ApiOperation({ summary: "Enable TOTP 2FA after verification" })
@ApiResponse({ status: 200, description: "2FA enabled successfully" }) @ApiResponse({ status: 200, description: "2FA enabled successfully" })
@@ -70,6 +72,7 @@ export class AuthController {
return this.twoFactorService.enable2FA(wildduckUserId, enable2FADto); return this.twoFactorService.enable2FA(wildduckUserId, enable2FADto);
} }
@AuthGuards()
@Post("verify") @Post("verify")
@ApiOperation({ summary: "Verify TOTP token" }) @ApiOperation({ summary: "Verify TOTP token" })
@ApiResponse({ status: 200, description: "2FA token verification result" }) @ApiResponse({ status: 200, description: "2FA token verification result" })
@@ -77,6 +80,7 @@ export class AuthController {
return this.twoFactorService.verify2FA(wildduckUserId, verify2FADto); return this.twoFactorService.verify2FA(wildduckUserId, verify2FADto);
} }
@AuthGuards()
@Get("status") @Get("status")
@ApiOperation({ summary: "Get 2FA status for user" }) @ApiOperation({ summary: "Get 2FA status for user" })
@ApiResponse({ status: 200, description: "2FA status retrieved successfully" }) @ApiResponse({ status: 200, description: "2FA status retrieved successfully" })
@@ -85,6 +89,7 @@ export class AuthController {
return this.twoFactorService.get2FAStatus(wildduckUserId); return this.twoFactorService.get2FAStatus(wildduckUserId);
} }
@AuthGuards()
@Delete("disable") @Delete("disable")
@ApiOperation({ summary: "Disable 2FA for user" }) @ApiOperation({ summary: "Disable 2FA for user" })
@ApiResponse({ status: 200, description: "2FA disabled successfully" }) @ApiResponse({ status: 200, description: "2FA disabled successfully" })
@@ -93,6 +98,7 @@ export class AuthController {
return this.twoFactorService.disable2FA(wildduckUserId); return this.twoFactorService.disable2FA(wildduckUserId);
} }
@AuthGuards()
@Post("backup-codes/generate") @Post("backup-codes/generate")
@ApiOperation({ summary: "Generate new backup codes" }) @ApiOperation({ summary: "Generate new backup codes" })
@ApiResponse({ status: 200, description: "Backup codes generated successfully" }) @ApiResponse({ status: 200, description: "Backup codes generated successfully" })
@@ -101,6 +107,7 @@ export class AuthController {
return this.twoFactorService.generateBackupCodes(wildduckUserId); return this.twoFactorService.generateBackupCodes(wildduckUserId);
} }
@AuthGuards()
@Post("backup-codes/use") @Post("backup-codes/use")
@ApiOperation({ summary: "Use backup code for authentication" }) @ApiOperation({ summary: "Use backup code for authentication" })
@ApiResponse({ status: 200, description: "Backup code verification result" }) @ApiResponse({ status: 200, description: "Backup code verification result" })
@@ -21,14 +21,14 @@ export class TwoFactorService {
const response = await firstValueFrom( const response = await firstValueFrom(
this.mailServerService.twoFactor.setupTOTP(wildduckUserId, { this.mailServerService.twoFactor.setupTOTP(wildduckUserId, {
issuer: setup2FADto.issuer || "DMail Service", issuer: setup2FADto.issuer || "DMail Service",
fresh: setup2FADto.fresh || false, label: setup2FADto.label || "DMail Service",
}), }),
); );
return { return {
message: TwoFactorMessage.TWO_FACTOR_SETUP_INITIATED, message: TwoFactorMessage.TWO_FACTOR_SETUP_INITIATED,
qr: response.qrcode, qr: response.qrcode,
secret: response.seed, seed: response.seed,
}; };
} }
@@ -33,10 +33,7 @@ export class EmailMonitoringService implements OnModuleInit {
await this.startMonitoring(); await this.startMonitoring();
} }
//****************************************************** */ //==============================================
// MONITORING SCHEDULER FUNCTIONALITY
//****************************************************** */
async startMonitoring() { async startMonitoring() {
if (this.isMonitoringActive) { if (this.isMonitoringActive) {
this.logger.warn("Email monitoring is already running"); this.logger.warn("Email monitoring is already running");
@@ -104,9 +101,7 @@ export class EmailMonitoringService implements OnModuleInit {
await this.startMonitoring(); await this.startMonitoring();
} }
//****************************************************** */ //==============================================
// CORE MONITORING FUNCTIONALITY
//****************************************************** */
async scheduleEmailCheck() { async scheduleEmailCheck() {
try { try {
@@ -155,6 +150,8 @@ export class EmailMonitoringService implements OnModuleInit {
} }
} }
//==============================================
async processUserEmailCheck(job: EmailMonitoringJob) { async processUserEmailCheck(job: EmailMonitoringJob) {
const { userId, lastCheckTimestamp } = job; const { userId, lastCheckTimestamp } = job;
@@ -202,6 +199,8 @@ export class EmailMonitoringService implements OnModuleInit {
} }
} }
//==============================================
private async processNewEmailMessage(userId: string, _wildduckUserId: string, message: Record<string, any>) { private async processNewEmailMessage(userId: string, _wildduckUserId: string, message: Record<string, any>) {
try { try {
if (message.seen) { if (message.seen) {
@@ -221,11 +220,21 @@ export class EmailMonitoringService implements OnModuleInit {
timestamp: message.timestamp, timestamp: message.timestamp,
}; };
await this.notificationQueue.addNewEmailNotification(userId, { // Check if message.from exists and has at least one element
fromEmail: message.from[0].address, if (message.from && Array.isArray(message.from) && message.from.length > 0 && message.from[0]) {
fromName: message.from[0].name, await this.notificationQueue.addNewEmailNotification(userId, {
subject: message.subject, fromEmail: message.from[0].address || "unknown@example.com",
}); fromName: message.from[0].name || "Unknown Sender",
subject: message.subject || "No Subject",
});
} else {
this.logger.warn(`Email message ${message.id} for user ${userId} has invalid 'from' field`);
await this.notificationQueue.addNewEmailNotification(userId, {
fromEmail: "unknown@example.com",
fromName: "Unknown Sender",
subject: message.subject || "No Subject",
});
}
await this.emailGateway.notifyNewEmail(userId, emailPayload); await this.emailGateway.notifyNewEmail(userId, emailPayload);
} catch (error) { } catch (error) {
@@ -233,6 +242,8 @@ export class EmailMonitoringService implements OnModuleInit {
} }
} }
//==============================================
private async updateUnreadCount(userId: string, wildduckUserId: string, userEmailAddress?: string) { private async updateUnreadCount(userId: string, wildduckUserId: string, userEmailAddress?: string) {
try { try {
const inboxMailboxId = await this.mailboxResolver.getInboxMailboxId(wildduckUserId); const inboxMailboxId = await this.mailboxResolver.getInboxMailboxId(wildduckUserId);
@@ -264,12 +275,15 @@ export class EmailMonitoringService implements OnModuleInit {
} }
} }
async checkUserEmails(userId: string): Promise<void> { //==============================================
async checkUserEmails(userId: string, wildduckUserId: string): Promise<void> {
await this.monitoringQueue.add( await this.monitoringQueue.add(
EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE_PROCESSING, EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE_PROCESSING,
{ {
userId, userId,
lastCheckTimestamp: new Date(Date.now() - 60000), // Check emails from last minute wildduckUserId: wildduckUserId,
lastCheckTimestamp: new Date(Date.now() - 60000),
}, },
{ {
attempts: 3, attempts: 3,
@@ -279,9 +293,7 @@ export class EmailMonitoringService implements OnModuleInit {
); );
} }
//****************************************************** */ //==============================================
// STATUS AND MANAGEMENT FUNCTIONALITY
//****************************************************** */
async getMonitoringStatus() { async getMonitoringStatus() {
try { try {
@@ -308,6 +320,8 @@ export class EmailMonitoringService implements OnModuleInit {
} }
} }
//==============================================
async getQueueStatus() { async getQueueStatus() {
const [waiting, active, completed, failed] = await Promise.all([ const [waiting, active, completed, failed] = await Promise.all([
this.monitoringQueue.getWaiting(), this.monitoringQueue.getWaiting(),
@@ -324,6 +338,8 @@ export class EmailMonitoringService implements OnModuleInit {
}; };
} }
//==============================================
async getDetailedQueueInfo() { async getDetailedQueueInfo() {
try { try {
const [waiting, active, completed, failed, repeatableJobs] = await Promise.all([ const [waiting, active, completed, failed, repeatableJobs] = await Promise.all([
@@ -364,6 +380,8 @@ export class EmailMonitoringService implements OnModuleInit {
} }
} }
//==============================================
async cleanupJobs() { async cleanupJobs() {
try { try {
// Clean completed jobs older than 30 minutes // Clean completed jobs older than 30 minutes
@@ -16,7 +16,6 @@ export interface TOTPSetupResponse {
success: true; success: true;
qrcode: string; qrcode: string;
seed: string; seed: string;
secret?: string;
} }
export interface U2FRegistrationStartResponse { export interface U2FRegistrationStartResponse {
@@ -1,3 +1,8 @@
export interface BaseTwoFactorData {
sess?: string;
ip?: string;
}
export interface TwoFactorTOTP { export interface TwoFactorTOTP {
enabled: boolean; enabled: boolean;
issuer?: string; issuer?: string;
@@ -18,35 +23,32 @@ export interface BackupCode {
used_time?: string; used_time?: string;
} }
export interface TOTPSetupData { export interface TOTPSetupData extends BaseTwoFactorData {
issuer?: string; issuer?: string;
fresh?: boolean; label?: string;
sess?: string;
ip?: string;
} }
export interface TOTPVerificationData { export interface TOTPVerificationData extends BaseTwoFactorData {
token: string; token: string;
sess?: string;
ip?: string;
} }
export interface U2FRegistrationData { export interface U2FRegistrationData extends BaseTwoFactorData {
requestId: string; requestId: string;
response: any; response: any;
sess?: string;
ip?: string;
} }
export interface U2FAuthenticationData { export interface U2FAuthenticationData extends BaseTwoFactorData {
requestId: string; requestId: string;
response: any; response: any;
sess?: string;
ip?: string;
} }
export interface BackupCodeData { export interface BackupCodeData extends BaseTwoFactorData {
code: string; code: string;
sess?: string; }
ip?: string;
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TwoFactorDisableData extends BaseTwoFactorData {}
export interface BackupCodesGenerateData extends BaseTwoFactorData {
count?: number;
} }
@@ -8,6 +8,14 @@ import {
TOTPSetupResponse, TOTPSetupResponse,
TwoFactorStatusResponse, TwoFactorStatusResponse,
} from "../interfaces/two-factor-response.interface"; } from "../interfaces/two-factor-response.interface";
import {
BackupCodeData,
BaseTwoFactorData,
TOTPSetupData,
TOTPVerificationData,
U2FAuthenticationData,
U2FRegistrationData,
} from "../interfaces/two-factor.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface"; import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable() @Injectable()
@@ -23,43 +31,21 @@ export class WildDuckTwoFactorService extends WildDuckBaseService {
/** /**
* Setup TOTP 2FA * Setup TOTP 2FA
*/ */
setupTOTP( setupTOTP(userId: string, data: TOTPSetupData): Observable<TOTPSetupResponse> {
userId: string,
data?: {
issuer?: string;
fresh?: boolean;
sess?: string;
ip?: string;
},
): Observable<TOTPSetupResponse> {
return this.post<TOTPSetupResponse>(`/users/${userId}/2fa/totp/setup`, data); return this.post<TOTPSetupResponse>(`/users/${userId}/2fa/totp/setup`, data);
} }
/** /**
* Enable TOTP 2FA * Enable TOTP 2FA
*/ */
enableTOTP( enableTOTP(userId: string, data: TOTPVerificationData): Observable<WildDuckSuccessResponse> {
userId: string,
data: {
token: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/enable`, data); return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/enable`, data);
} }
/** /**
* Check TOTP token * Check TOTP token
*/ */
checkTOTP( checkTOTP(userId: string, data: TOTPVerificationData): Observable<WildDuckSuccessResponse> {
userId: string,
data: {
token: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/check`, data); return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/check`, data);
} }
@@ -82,10 +68,7 @@ export class WildDuckTwoFactorService extends WildDuckBaseService {
*/ */
startU2FRegistration( startU2FRegistration(
userId: string, userId: string,
data?: { data?: U2FRegistrationData,
sess?: string;
ip?: string;
},
): Observable< ): Observable<
WildDuckSuccessResponse<{ WildDuckSuccessResponse<{
requestId: string; requestId: string;
@@ -103,15 +86,7 @@ export class WildDuckTwoFactorService extends WildDuckBaseService {
/** /**
* Complete U2F registration * Complete U2F registration
*/ */
completeU2FRegistration( completeU2FRegistration(userId: string, data: U2FRegistrationData): Observable<WildDuckSuccessResponse> {
userId: string,
data: {
requestId: string;
response: any;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/u2f/enable`, data); return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/u2f/enable`, data);
} }
@@ -120,10 +95,7 @@ export class WildDuckTwoFactorService extends WildDuckBaseService {
*/ */
startU2FAuthentication( startU2FAuthentication(
userId: string, userId: string,
data?: { data?: U2FAuthenticationData,
sess?: string;
ip?: string;
},
): Observable< ): Observable<
WildDuckSuccessResponse<{ WildDuckSuccessResponse<{
requestId: string; requestId: string;
@@ -141,15 +113,7 @@ export class WildDuckTwoFactorService extends WildDuckBaseService {
/** /**
* Complete U2F authentication * Complete U2F authentication
*/ */
completeU2FAuthentication( completeU2FAuthentication(userId: string, data: U2FAuthenticationData): Observable<WildDuckSuccessResponse> {
userId: string,
data: {
requestId: string;
response: any;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/u2f/check`, data); return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/u2f/check`, data);
} }
@@ -170,13 +134,7 @@ export class WildDuckTwoFactorService extends WildDuckBaseService {
/** /**
* Generate backup codes * Generate backup codes
*/ */
generateBackupCodes( generateBackupCodes(userId: string, data?: BackupCodeData): Observable<BackupCodesGenerateResponse> {
userId: string,
data?: {
sess?: string;
ip?: string;
},
): Observable<BackupCodesGenerateResponse> {
return this.post<BackupCodesGenerateResponse>(`/users/${userId}/2fa/backup/generate`, data); return this.post<BackupCodesGenerateResponse>(`/users/${userId}/2fa/backup/generate`, data);
} }
@@ -190,27 +148,14 @@ export class WildDuckTwoFactorService extends WildDuckBaseService {
/** /**
* Use backup code * Use backup code
*/ */
useBackupCode( useBackupCode(userId: string, data: BackupCodeData): Observable<WildDuckSuccessResponse> {
userId: string,
data: {
code: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/backup/use`, data); return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/backup/use`, data);
} }
/** /**
* Disable all 2FA methods * Disable all 2FA methods
*/ */
disableAll2FA( disableAll2FA(userId: string, params?: BaseTwoFactorData): Observable<WildDuckSuccessResponse> {
userId: string,
params?: {
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/2fa`, params); return this.delete<WildDuckSuccessResponse>(`/users/${userId}/2fa`, params);
} }
} }