chore: add 2fa
This commit is contained in:
@@ -6,36 +6,341 @@ export class MailServerException extends HttpException {
|
||||
}
|
||||
}
|
||||
|
||||
// User-related exceptions
|
||||
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 UserQuotaExceededException extends MailServerException {
|
||||
constructor(userId: string, quotaType?: string) {
|
||||
super(`User '${userId}' has exceeded ${quotaType || "storage"} quota`, HttpStatus.INSUFFICIENT_STORAGE);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDisabledException extends MailServerException {
|
||||
constructor(userId: string) {
|
||||
super(`User '${userId}' is disabled`, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication exceptions
|
||||
export class AuthenticationFailedException extends MailServerException {
|
||||
constructor(details?: string) {
|
||||
super(`Authentication failed${details ? `: ${details}` : ""}`, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidCredentialsException extends MailServerException {
|
||||
constructor() {
|
||||
super("Invalid username or password", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordExpiredException extends MailServerException {
|
||||
constructor() {
|
||||
super("Password has expired and must be changed", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
export class TooManyLoginAttemptsException extends MailServerException {
|
||||
constructor() {
|
||||
super("Too many login attempts, account temporarily locked", HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
|
||||
// 2FA exceptions
|
||||
export class TwoFactorRequiredException extends MailServerException {
|
||||
constructor(methods: string[]) {
|
||||
super(`Two-factor authentication required. Available methods: ${methods.join(", ")}`, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidTwoFactorTokenException extends MailServerException {
|
||||
constructor(method?: string) {
|
||||
super(`Invalid ${method || "2FA"} token provided`, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
export class TwoFactorNotSetupException extends MailServerException {
|
||||
constructor(method?: string) {
|
||||
super(`${method || "Two-factor authentication"} is not setup for this user`, HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
export class TwoFactorAlreadySetupException extends MailServerException {
|
||||
constructor(method?: string) {
|
||||
super(`${method || "Two-factor authentication"} is already setup for this user`, HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidBackupCodeException extends MailServerException {
|
||||
constructor() {
|
||||
super("Invalid or already used backup code", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoBackupCodesException extends MailServerException {
|
||||
constructor() {
|
||||
super("No backup codes available for this user", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
// Mailbox exceptions
|
||||
export class MailboxNotFoundException extends MailServerException {
|
||||
constructor(mailboxId: string) {
|
||||
super(`Mailbox with ID '${mailboxId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class MailboxAlreadyExistsException extends MailServerException {
|
||||
constructor(mailboxPath: string) {
|
||||
super(`Mailbox '${mailboxPath}' already exists`, HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
export class SpecialMailboxException extends MailServerException {
|
||||
constructor(operation: string) {
|
||||
super(`Cannot ${operation} special mailbox`, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Message exceptions
|
||||
export class MessageNotFoundException extends MailServerException {
|
||||
constructor(messageId: number) {
|
||||
super(`Message with ID '${messageId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageTooLargeException extends MailServerException {
|
||||
constructor(size?: number, limit?: number) {
|
||||
const sizeInfo = size && limit ? ` (${size} bytes, limit: ${limit} bytes)` : "";
|
||||
super(`Message is too large${sizeInfo}`, HttpStatus.PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidMessageFormatException extends MailServerException {
|
||||
constructor(details?: string) {
|
||||
super(`Invalid message format${details ? `: ${details}` : ""}`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageExpiredException extends MailServerException {
|
||||
constructor(messageId: number) {
|
||||
super(`Message with ID '${messageId}' has expired`, HttpStatus.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// Address exceptions
|
||||
export class AddressNotFoundException extends MailServerException {
|
||||
constructor(address: string) {
|
||||
super(`Address '${address}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddressAlreadyExistsException extends MailServerException {
|
||||
constructor(address: string) {
|
||||
super(`Address '${address}' already exists`, HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidAddressFormatException extends MailServerException {
|
||||
constructor(address: string) {
|
||||
super(`Invalid email address format: '${address}'`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddressQuotaExceededException extends MailServerException {
|
||||
constructor(userId: string) {
|
||||
super(`User '${userId}' has reached maximum number of addresses`, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Validation exceptions
|
||||
export class ValidationException extends MailServerException {
|
||||
constructor(field: string, value?: string) {
|
||||
super(`Validation failed for field '${field}'${value ? ` with value '${value}'` : ""}`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
export class RequiredFieldException extends MailServerException {
|
||||
constructor(field: string) {
|
||||
super(`Required field '${field}' is missing`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidParameterException extends MailServerException {
|
||||
constructor(parameter: string, expectedType?: string) {
|
||||
super(`Invalid parameter '${parameter}'${expectedType ? `, expected ${expectedType}` : ""}`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateValueException extends MailServerException {
|
||||
constructor(field: string, value: string) {
|
||||
super(`Duplicate value '${value}' for field '${field}'`, HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
// Storage and quota exceptions
|
||||
export class StorageQuotaExceededException extends MailServerException {
|
||||
constructor(used?: number, limit?: number) {
|
||||
const quotaInfo = used && limit ? ` (used: ${used}MB, limit: ${limit}MB)` : "";
|
||||
super(`Storage quota exceeded${quotaInfo}`, HttpStatus.INSUFFICIENT_STORAGE);
|
||||
}
|
||||
}
|
||||
|
||||
export class RecipientQuotaExceededException extends MailServerException {
|
||||
constructor(count?: number, limit?: number) {
|
||||
const quotaInfo = count && limit ? ` (current: ${count}, limit: ${limit})` : "";
|
||||
super(`Recipient quota exceeded${quotaInfo}`, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectionQuotaExceededException extends MailServerException {
|
||||
constructor(type: string, count?: number, limit?: number) {
|
||||
const quotaInfo = count && limit ? ` (current: ${count}, limit: ${limit})` : "";
|
||||
super(`${type} connection quota exceeded${quotaInfo}`, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter exceptions
|
||||
export class FilterNotFoundException extends MailServerException {
|
||||
constructor(filterId: string) {
|
||||
super(`Filter with ID '${filterId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidFilterRuleException extends MailServerException {
|
||||
constructor(details?: string) {
|
||||
super(`Invalid filter rule${details ? `: ${details}` : ""}`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// DKIM exceptions
|
||||
export class DKIMNotFoundException extends MailServerException {
|
||||
constructor(domain: string) {
|
||||
super(`DKIM key not found for domain '${domain}'`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidDKIMKeyException extends MailServerException {
|
||||
constructor(details?: string) {
|
||||
super(`Invalid DKIM key${details ? `: ${details}` : ""}`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// Submission exceptions
|
||||
export class SubmissionFailedException extends MailServerException {
|
||||
constructor(details?: string) {
|
||||
super(`Message submission failed${details ? `: ${details}` : ""}`, HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
}
|
||||
|
||||
export class RateLimitExceededException extends MailServerException {
|
||||
constructor(type?: string) {
|
||||
super(`Rate limit exceeded${type ? ` for ${type}` : ""}`, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
|
||||
// Application Password exceptions
|
||||
export class ApplicationPasswordNotFoundException extends MailServerException {
|
||||
constructor(aspId: string) {
|
||||
super(`Application password with ID '${aspId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApplicationPasswordExpiredException extends MailServerException {
|
||||
constructor(aspId: string) {
|
||||
super(`Application password with ID '${aspId}' has expired`, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
// Export/Import exceptions
|
||||
export class ExportNotFoundException extends MailServerException {
|
||||
constructor(exportId: string) {
|
||||
super(`Export with ID '${exportId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExportInProgressException extends MailServerException {
|
||||
constructor(exportId: string) {
|
||||
super(`Export with ID '${exportId}' is still in progress`, HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
export class ImportFailedException extends MailServerException {
|
||||
constructor(details?: string) {
|
||||
super(`Import failed${details ? `: ${details}` : ""}`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// Audit exceptions
|
||||
export class AuditLogNotFoundException extends MailServerException {
|
||||
constructor(entryId: string) {
|
||||
super(`Audit log entry with ID '${entryId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook exceptions
|
||||
export class WebhookNotFoundException extends MailServerException {
|
||||
constructor(webhookId: string) {
|
||||
super(`Webhook with ID '${webhookId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class WebhookDeliveryFailedException extends MailServerException {
|
||||
constructor(url: string, details?: string) {
|
||||
super(`Webhook delivery failed to '${url}'${details ? `: ${details}` : ""}`, HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
}
|
||||
|
||||
// Autoreply exceptions
|
||||
export class AutoreplyAlreadyActiveException extends MailServerException {
|
||||
constructor(userId: string) {
|
||||
super(`Autoreply is already active for user '${userId}'`, HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoreplyNotActiveException extends MailServerException {
|
||||
constructor(userId: string) {
|
||||
super(`Autoreply is not active for user '${userId}'`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
// Archive exceptions
|
||||
export class ArchiveNotFoundException extends MailServerException {
|
||||
constructor(archiveId: string) {
|
||||
super(`Archive with ID '${archiveId}' not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
export class ArchiveCorruptedException extends MailServerException {
|
||||
constructor(archiveId: string) {
|
||||
super(`Archive with ID '${archiveId}' is corrupted`, HttpStatus.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
// Domain Access exceptions
|
||||
export class DomainAccessForbiddenException extends MailServerException {
|
||||
constructor(domain: string) {
|
||||
super(`Access to domain '${domain}' is forbidden`, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
export class DomainNotAllowedException extends MailServerException {
|
||||
constructor(domain: string) {
|
||||
super(`Domain '${domain}' is not allowed`, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Connection and server exceptions
|
||||
export class MailServerConnectionException extends MailServerException {
|
||||
constructor(details?: string) {
|
||||
super(`Mail server connection failed${details ? `: ${details}` : ""}`, HttpStatus.SERVICE_UNAVAILABLE);
|
||||
@@ -47,3 +352,15 @@ export class InvalidMailServerResponseException extends MailServerException {
|
||||
super("Invalid response from mail server", HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
}
|
||||
|
||||
export class MailServerTimeoutException extends MailServerException {
|
||||
constructor(operation?: string) {
|
||||
super(`Mail server timeout${operation ? ` during ${operation}` : ""}`, HttpStatus.GATEWAY_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
export class MailServerMaintenanceException extends MailServerException {
|
||||
constructor() {
|
||||
super("Mail server is under maintenance", HttpStatus.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ async function bootstrap() {
|
||||
|
||||
app.useGlobalInterceptors(new ResponseInterceptor(), new PaginationInterceptor());
|
||||
|
||||
app.useGlobalFilters(new MailServerExceptionFilter(), new HttpExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter(), new MailServerExceptionFilter());
|
||||
|
||||
app.enableCors({
|
||||
origin: true,
|
||||
|
||||
@@ -25,6 +25,12 @@ export class Verify2FADto {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export class Disable2FADto {
|
||||
@ApiProperty({ description: "TOTP token or backup code for verification", example: "123456" })
|
||||
@IsString()
|
||||
token: string;
|
||||
}
|
||||
|
||||
export class UseBackupCodeDto {
|
||||
@ApiProperty({ description: "Backup code", example: "abcd1234" })
|
||||
@IsString()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Patch, Post } from "@nestjs/common";
|
||||
import { Body, Controller, HttpCode, HttpStatus, Patch, Post } from "@nestjs/common";
|
||||
import { ApiBadRequestResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger";
|
||||
|
||||
import { ChangePasswordDto } from "./DTO/change-password.dto";
|
||||
import { Enable2FADto, GenerateBackupCodesDto, UseBackupCodeDto, Verify2FADto } from "./DTO/create-two-factor.dto";
|
||||
import { Disable2FADto, Enable2FADto, GenerateBackupCodesDto, UseBackupCodeDto, Verify2FADto } from "./DTO/create-two-factor.dto";
|
||||
import { LoginDto } from "./DTO/login.dto";
|
||||
import { RefreshTokenDto } from "./DTO/refresh-token.dto";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
@@ -81,21 +81,12 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
@Get("2fa/status")
|
||||
@ApiOperation({ summary: "Get 2FA status for user" })
|
||||
@ApiResponse({ status: 200, description: "2FA status retrieved successfully" })
|
||||
@ApiResponse({ status: 400, description: "Failed to get 2FA status" })
|
||||
get2FAStatus(@UserDec("wildduckUserId") wildduckUserId: string) {
|
||||
return this.twoFactorService.get2FAStatus(wildduckUserId);
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
@Delete("2fa/disable")
|
||||
@Post("2fa/disable")
|
||||
@ApiOperation({ summary: "Disable 2FA for user" })
|
||||
@ApiResponse({ status: 200, description: "2FA disabled successfully" })
|
||||
@ApiResponse({ status: 400, description: "Failed to disable 2FA" })
|
||||
disable2FA(@UserDec("wildduckUserId") wildduckUserId: string) {
|
||||
return this.twoFactorService.disable2FA(wildduckUserId);
|
||||
disable2FA(@UserDec("wildduckUserId") wildduckUserId: string, @Body() disable2FADto: Disable2FADto) {
|
||||
return this.twoFactorService.disable2FA(wildduckUserId, disable2FADto);
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
|
||||
@@ -29,7 +29,12 @@ export class AuthService {
|
||||
if (!user.isActive) throw new BadRequestException(AuthMessage.USER_ACCOUNT_INACTIVE);
|
||||
|
||||
if (user.is2FAEnabled) {
|
||||
if (!twoFactorToken) throw new BadRequestException(TwoFactorMessage.TWO_FACTOR_TOKEN_REQUIRED);
|
||||
if (!twoFactorToken) {
|
||||
return {
|
||||
message: TwoFactorMessage.TWO_FACTOR_TOKEN_REQUIRED,
|
||||
requires2FA: true,
|
||||
};
|
||||
}
|
||||
|
||||
const is2FAValid = await this.twoFactorService.validateTwoFactorToken(user, twoFactorToken);
|
||||
if (!is2FAValid) throw new BadRequestException(TwoFactorMessage.TWO_FACTOR_TOKEN_INVALID);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TwoFactorMessage, UserMessage } from "../../../common/enums/message.enu
|
||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { UserRepository } from "../../users/repositories/user.repository";
|
||||
import { Enable2FADto, UseBackupCodeDto, Verify2FADto } from "../DTO/create-two-factor.dto";
|
||||
import { Disable2FADto, Enable2FADto, UseBackupCodeDto, Verify2FADto } from "../DTO/create-two-factor.dto";
|
||||
|
||||
@Injectable()
|
||||
export class TwoFactorService {
|
||||
@@ -72,27 +72,14 @@ export class TwoFactorService {
|
||||
}
|
||||
|
||||
//==============================================
|
||||
async get2FAStatus(wildduckUserId: string) {
|
||||
async disable2FA(wildduckUserId: string, disable2FADto: Disable2FADto) {
|
||||
const user = await this.userRepository.findOne({ wildduckUserId });
|
||||
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const response = await firstValueFrom(this.mailServerService.twoFactor.get2FAStatus(wildduckUserId));
|
||||
if (!user.is2FAEnabled) throw new BadRequestException(TwoFactorMessage.TWO_FACTOR_NOT_ENABLED);
|
||||
|
||||
return {
|
||||
enabled: user.is2FAEnabled,
|
||||
enabledMethods: response.enabled2fa,
|
||||
totpEnabled: response.enabled2fa.includes("totp"),
|
||||
backupCodesAvailable: response.enabled2fa.includes("backup"),
|
||||
enabledAt: user.twoFactorEnabledAt,
|
||||
};
|
||||
}
|
||||
|
||||
//==============================================
|
||||
async disable2FA(wildduckUserId: string) {
|
||||
const user = await this.userRepository.findOne({ wildduckUserId });
|
||||
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
await this.validateTwoFactorToken(user, disable2FADto.token);
|
||||
|
||||
await firstValueFrom(this.mailServerService.twoFactor.disableAll2FA(wildduckUserId));
|
||||
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
export interface TwoFactorStatusResponse {
|
||||
success: true;
|
||||
enabled2fa: string[];
|
||||
totp?: {
|
||||
enabled: boolean;
|
||||
url?: string;
|
||||
label?: string;
|
||||
};
|
||||
u2f?: {
|
||||
enabled: boolean;
|
||||
keyHandle?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TOTPSetupResponse {
|
||||
success: true;
|
||||
qrcode: string;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Observable, catchError, map, throwError } from "rxjs";
|
||||
|
||||
import * as MailServerExceptions from "../../../core/exceptions/mail-server.exceptions";
|
||||
import { MailServerException } from "../../../core/exceptions/mail-server.exceptions";
|
||||
import { WildDuckErrorResponse, WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
|
||||
|
||||
@@ -19,17 +20,15 @@ export class WildDuckBaseService {
|
||||
this.baseURL = this.configService.getOrThrow<string>("WILDDUCK_BASE_URL");
|
||||
}
|
||||
|
||||
protected buildUrl(path: string, params?: Record<string, unknown>): string {
|
||||
protected buildUrl(path: string, params?: Record<string, any>): string {
|
||||
const url = new URL(path, this.baseURL);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.set(key, String(value));
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] !== undefined && params[key] !== null) {
|
||||
url.searchParams.append(key, params[key].toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
@@ -41,7 +40,7 @@ export class WildDuckBaseService {
|
||||
const data = response.data;
|
||||
|
||||
if ("success" in data && data.success === false) {
|
||||
throw new MailServerException(data.error || "WildDuck API error");
|
||||
throw this.handleMailServerError(data.error || "WildDuck API error", response.status);
|
||||
}
|
||||
|
||||
if ("success" in data && data.success === true) {
|
||||
@@ -54,14 +53,57 @@ export class WildDuckBaseService {
|
||||
this.logger.error(`WildDuck API error: ${error.message}`, error.stack);
|
||||
|
||||
if (error.response?.data?.error) {
|
||||
return throwError(() => new MailServerException(error.response.data.error));
|
||||
return throwError(() => this.handleMailServerError(error.response.data.error, error.response.status));
|
||||
}
|
||||
|
||||
return throwError(() => error);
|
||||
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
||||
return throwError(() => new MailServerExceptions.MailServerConnectionException(error.message));
|
||||
}
|
||||
|
||||
return throwError(() => new MailServerException(error.message || "Unknown mail server error"));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mail server errors and throw appropriate exceptions
|
||||
*/
|
||||
protected handleMailServerError(errorMessage: string, statusCode?: number): MailServerException {
|
||||
// Map HTTP status codes to appropriate exceptions
|
||||
if (statusCode) {
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
return new MailServerExceptions.AuthenticationFailedException(errorMessage);
|
||||
case 403:
|
||||
return new MailServerExceptions.UserDisabledException("user");
|
||||
case 404:
|
||||
return new MailServerExceptions.UserNotFoundException("unknown");
|
||||
case 409:
|
||||
return new MailServerExceptions.UserAlreadyExistsException("user");
|
||||
case 413:
|
||||
return new MailServerExceptions.MessageTooLargeException();
|
||||
case 429:
|
||||
return new MailServerExceptions.TooManyLoginAttemptsException();
|
||||
case 502:
|
||||
return new MailServerExceptions.InvalidMailServerResponseException();
|
||||
case 503:
|
||||
return new MailServerExceptions.MailServerMaintenanceException();
|
||||
case 504:
|
||||
return new MailServerExceptions.MailServerTimeoutException();
|
||||
case 507:
|
||||
return new MailServerExceptions.StorageQuotaExceededException();
|
||||
default:
|
||||
if (statusCode >= 500) {
|
||||
return new MailServerExceptions.InvalidMailServerResponseException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to generic mail server exception with the error message
|
||||
return new MailServerException(errorMessage);
|
||||
}
|
||||
|
||||
protected get<T>(path: string, params?: Record<string, any>): Observable<T> {
|
||||
const url = this.buildUrl(path, params);
|
||||
this.logger.debug(`WildDuck GET: ${url}`);
|
||||
@@ -81,6 +123,6 @@ export class WildDuckBaseService {
|
||||
|
||||
protected delete<T>(path: string, params?: Record<string, any>): Observable<T> {
|
||||
const url = this.buildUrl(path, params);
|
||||
return this.handleResponse<T>(this.httpService.delete(url));
|
||||
return this.handleResponse<T>(this.httpService.delete(url, { params }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Observable } from "rxjs";
|
||||
import { catchError, map, throwError } from "rxjs";
|
||||
|
||||
import { WildDuckBaseService } from "./wildduck-base.service";
|
||||
import * as MailServerExceptions from "../../../core/exceptions/mail-server.exceptions";
|
||||
import { MailServerException } from "../../../core/exceptions/mail-server.exceptions";
|
||||
import { ListMessagesQueryDto, SearchMessagesMailServerQueryDto, UpdateMessageDto, UploadMessageDto } from "../DTO/message.dto";
|
||||
import { AttachmentResponse } from "../interfaces/attachment.interface";
|
||||
@@ -101,10 +102,14 @@ export class WildDuckMessagesService extends WildDuckBaseService {
|
||||
this.logger.error(`WildDuck API error: ${error.message}`, error.stack);
|
||||
|
||||
if (error.response?.data?.error) {
|
||||
return throwError(() => new MailServerException(error.response.data.error));
|
||||
return throwError(() => this.handleMailServerError(error.response.data.error, error.response.status));
|
||||
}
|
||||
|
||||
return throwError(() => error);
|
||||
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
||||
return throwError(() => new MailServerExceptions.MailServerConnectionException(error.message));
|
||||
}
|
||||
|
||||
return throwError(() => new MailServerException(error.message || "Unknown mail server error"));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { WildDuckBaseService } from "./wildduck-base.service";
|
||||
import {
|
||||
BackupCodesGenerateResponse,
|
||||
BackupCodesListResponse,
|
||||
TOTPSetupResponse,
|
||||
TwoFactorStatusResponse,
|
||||
} from "../interfaces/two-factor-response.interface";
|
||||
import { BackupCodesGenerateResponse, BackupCodesListResponse, TOTPSetupResponse } from "../interfaces/two-factor-response.interface";
|
||||
import {
|
||||
BackupCodeData,
|
||||
BaseTwoFactorData,
|
||||
@@ -20,13 +15,6 @@ import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interfa
|
||||
|
||||
@Injectable()
|
||||
export class WildDuckTwoFactorService extends WildDuckBaseService {
|
||||
/**
|
||||
* Get 2FA status for user
|
||||
*/
|
||||
get2FAStatus(userId: string): Observable<TwoFactorStatusResponse> {
|
||||
return this.get<TwoFactorStatusResponse>(`/users/${userId}/2fa`);
|
||||
}
|
||||
|
||||
// TOTP Methods
|
||||
/**
|
||||
* Setup TOTP 2FA
|
||||
|
||||
Reference in New Issue
Block a user