chore: 2fa added
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
-- Add 2FA fields to users table
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN is_2fa_enabled BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
ADD COLUMN two_factor_enabled_at TIMESTAMPTZ DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Create index on is_2fa_enabled for query performance
|
||||||
|
CREATE INDEX idx_users_is_2fa_enabled ON users (is_2fa_enabled);
|
||||||
@@ -570,3 +570,19 @@ export const enum AiMessage {
|
|||||||
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید",
|
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید",
|
||||||
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست",
|
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum TwoFactorMessage {
|
||||||
|
TWO_FACTOR_SETUP_INITIATED = "راهاندازی احراز هویت دو مرحلهای با موفقیت آغاز شد",
|
||||||
|
TWO_FACTOR_ENABLED_SUCCESSFULLY = "احراز هویت دو مرحلهای با موفقیت فعال شد",
|
||||||
|
TWO_FACTOR_DISABLED_SUCCESSFULLY = "احراز هویت دو مرحلهای با موفقیت غیرفعال شد",
|
||||||
|
TWO_FACTOR_SETUP_FAILED = "راهاندازی احراز هویت دو مرحلهای ناموفق بود",
|
||||||
|
TWO_FACTOR_ENABLE_FAILED = "فعالسازی احراز هویت دو مرحلهای ناموفق بود",
|
||||||
|
TWO_FACTOR_TOKEN_VERIFIED_SUCCESSFULLY = "کد احراز هویت دو مرحلهای با موفقیت تأیید شد",
|
||||||
|
TWO_FACTOR_TOKEN_VERIFIED_FAILED = "کد احراز هویت دو مرحلهای نامعتبر میباشد",
|
||||||
|
TWO_FACTOR_NOT_ENABLED = "احراز هویت دو مرحلهای فعال نیست",
|
||||||
|
TWO_FACTOR_BACKUP_CODES_GENERATED_SUCCESSFULLY = "کدهای پشتیبان با موفقیت تولید شدند",
|
||||||
|
TWO_FACTOR_BACKUP_CODE_VERIFIED_SUCCESSFULLY = "کد پشتیبان با موفقیت تأیید شد",
|
||||||
|
TWO_FACTOR_BACKUP_CODE_VERIFIED_FAILED = "کد پشتیبان نامعتبر است",
|
||||||
|
TWO_FACTOR_TOKEN_REQUIRED = "کد احراز هویت دو مرحلهای الزامی است",
|
||||||
|
TWO_FACTOR_TOKEN_INVALID = "کد احراز هویت دو مرحلهای نامعتبر است",
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class Setup2FADto {
|
||||||
|
@ApiPropertyOptional({ description: "TOTP issuer name", example: "DMail Service" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
issuer?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Generate fresh secret", default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
fresh?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Enable2FADto {
|
||||||
|
@ApiProperty({ description: "TOTP token from authenticator app", example: "123456" })
|
||||||
|
@IsString()
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Verify2FADto {
|
||||||
|
@ApiProperty({ description: "TOTP token for verification", example: "123456" })
|
||||||
|
@IsString()
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UseBackupCodeDto {
|
||||||
|
@ApiProperty({ description: "Backup code", example: "abcd1234" })
|
||||||
|
@IsString()
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GenerateBackupCodesDto {
|
||||||
|
// Empty DTO for endpoint consistency
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
|
import { IsEmail, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
@@ -13,4 +13,9 @@ export class LoginDto {
|
|||||||
@IsString({ message: AuthMessage.INVALID_PASS_FORMAT })
|
@IsString({ message: AuthMessage.INVALID_PASS_FORMAT })
|
||||||
@ApiProperty({ description: "User's password", example: "SecurePassword123!" })
|
@ApiProperty({ description: "User's password", example: "SecurePassword123!" })
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiPropertyOptional({ description: "Two-factor authentication token (required if 2FA is enabled)", example: "123456" })
|
||||||
|
twoFactorToken?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Patch, Post } from "@nestjs/common";
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Patch, Post } from "@nestjs/common";
|
||||||
import { ApiBadRequestResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger";
|
import { ApiBadRequestResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { ChangePasswordDto } from "./DTO/change-password.dto";
|
import { ChangePasswordDto } from "./DTO/change-password.dto";
|
||||||
|
import { Enable2FADto, GenerateBackupCodesDto, Setup2FADto, UseBackupCodeDto, Verify2FADto } from "./DTO/create-two-factor.dto";
|
||||||
import { LoginDto } from "./DTO/login.dto";
|
import { LoginDto } from "./DTO/login.dto";
|
||||||
import { RefreshTokenDto } from "./DTO/refresh-token.dto";
|
import { RefreshTokenDto } from "./DTO/refresh-token.dto";
|
||||||
import { AuthService } from "./services/auth.service";
|
import { AuthService } from "./services/auth.service";
|
||||||
|
import { TwoFactorService } from "./services/two-factor.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
import { RefreshTokenRateLimit, StrictRateLimit } from "../../common/decorators/rate-limit.decorator";
|
import { RefreshTokenRateLimit, StrictRateLimit } from "../../common/decorators/rate-limit.decorator";
|
||||||
import { UserDec } from "../../common/decorators/user.decorator";
|
import { UserDec } from "../../common/decorators/user.decorator";
|
||||||
@@ -12,7 +14,10 @@ import { UserDec } from "../../common/decorators/user.decorator";
|
|||||||
@ApiTags("Authentication")
|
@ApiTags("Authentication")
|
||||||
@Controller("auth")
|
@Controller("auth")
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly twoFactorService: TwoFactorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@StrictRateLimit()
|
@StrictRateLimit()
|
||||||
@Post("login")
|
@Post("login")
|
||||||
@@ -48,4 +53,58 @@ export class AuthController {
|
|||||||
changePassword(@UserDec("id") userId: string, @Body() changePasswordDto: ChangePasswordDto) {
|
changePassword(@UserDec("id") userId: string, @Body() changePasswordDto: ChangePasswordDto) {
|
||||||
return this.authService.changePassword(userId, changePasswordDto);
|
return this.authService.changePassword(userId, changePasswordDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("setup")
|
||||||
|
@ApiOperation({ summary: "Setup TOTP 2FA for user" })
|
||||||
|
@ApiResponse({ status: 200, description: "2FA setup initiated successfully" })
|
||||||
|
@ApiResponse({ status: 400, description: "Failed to setup 2FA" })
|
||||||
|
setup2FA(@UserDec("wildduckUserId") wildduckUserId: string, @Body() setup2FADto: Setup2FADto) {
|
||||||
|
return this.twoFactorService.setup2FA(wildduckUserId, setup2FADto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("enable")
|
||||||
|
@ApiOperation({ summary: "Enable TOTP 2FA after verification" })
|
||||||
|
@ApiResponse({ status: 200, description: "2FA enabled successfully" })
|
||||||
|
@ApiResponse({ status: 400, description: "Invalid 2FA token or failed to enable 2FA" })
|
||||||
|
enable2FA(@UserDec("wildduckUserId") wildduckUserId: string, @Body() enable2FADto: Enable2FADto) {
|
||||||
|
return this.twoFactorService.enable2FA(wildduckUserId, enable2FADto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("verify")
|
||||||
|
@ApiOperation({ summary: "Verify TOTP token" })
|
||||||
|
@ApiResponse({ status: 200, description: "2FA token verification result" })
|
||||||
|
verify2FA(@UserDec("wildduckUserId") wildduckUserId: string, @Body() verify2FADto: Verify2FADto) {
|
||||||
|
return this.twoFactorService.verify2FA(wildduckUserId, verify2FADto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("backup-codes/generate")
|
||||||
|
@ApiOperation({ summary: "Generate new backup codes" })
|
||||||
|
@ApiResponse({ status: 200, description: "Backup codes generated successfully" })
|
||||||
|
@ApiResponse({ status: 400, description: "2FA must be enabled to generate backup codes" })
|
||||||
|
generateBackupCodes(@UserDec("wildduckUserId") wildduckUserId: string, @Body() _generateBackupCodesDto: GenerateBackupCodesDto) {
|
||||||
|
return this.twoFactorService.generateBackupCodes(wildduckUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("backup-codes/use")
|
||||||
|
@ApiOperation({ summary: "Use backup code for authentication" })
|
||||||
|
@ApiResponse({ status: 200, description: "Backup code verification result" })
|
||||||
|
useBackupCode(@UserDec("wildduckUserId") wildduckUserId: string, @Body() useBackupCodeDto: UseBackupCodeDto) {
|
||||||
|
return this.twoFactorService.useBackupCode(wildduckUserId, useBackupCodeDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { JwtModule } from "@nestjs/jwt";
|
|||||||
import { PassportModule } from "@nestjs/passport";
|
import { PassportModule } from "@nestjs/passport";
|
||||||
|
|
||||||
import { AuthController } from "./auth.controller";
|
import { AuthController } from "./auth.controller";
|
||||||
import { jwtConfig } from "../../configs/jwt.config";
|
|
||||||
import { MailServerModule } from "../mail-server/mail-server.module";
|
|
||||||
import { UtilsModule } from "../utils/utils.module";
|
|
||||||
import { AuthService } from "./services/auth.service";
|
import { AuthService } from "./services/auth.service";
|
||||||
import { TokensService } from "./services/tokens.service";
|
import { TokensService } from "./services/tokens.service";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { TwoFactorService } from "./services/two-factor.service";
|
||||||
import { ConsoleJwtStrategy } from "./strategies/console-jwt.strategy";
|
import { ConsoleJwtStrategy } from "./strategies/console-jwt.strategy";
|
||||||
import { LocalJwtStrategy } from "./strategies/local-jwt.strategy";
|
import { LocalJwtStrategy } from "./strategies/local-jwt.strategy";
|
||||||
|
import { jwtConfig } from "../../configs/jwt.config";
|
||||||
import { BusinessesModule } from "../businesses/businesses.module";
|
import { BusinessesModule } from "../businesses/businesses.module";
|
||||||
|
import { MailServerModule } from "../mail-server/mail-server.module";
|
||||||
import { NotificationModule } from "../notifications/notifications.module";
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
import { User } from "../users/entities/user.entity";
|
import { User } from "../users/entities/user.entity";
|
||||||
|
import { UsersModule } from "../users/users.module";
|
||||||
|
import { UtilsModule } from "../utils/utils.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -28,7 +29,7 @@ import { User } from "../users/entities/user.entity";
|
|||||||
NotificationModule,
|
NotificationModule,
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, TokensService, LocalJwtStrategy, ConsoleJwtStrategy],
|
providers: [AuthService, TwoFactorService, TokensService, LocalJwtStrategy, ConsoleJwtStrategy],
|
||||||
exports: [AuthService],
|
exports: [AuthService, TwoFactorService, TokensService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { TokensService } from "./tokens.service";
|
import { TokensService } from "./tokens.service";
|
||||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
import { TwoFactorService } from "./two-factor.service";
|
||||||
|
import { AuthMessage, TwoFactorMessage } from "../../../common/enums/message.enum";
|
||||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||||
import { UserRepository } from "../../users/repositories/user.repository";
|
import { UserRepository } from "../../users/repositories/user.repository";
|
||||||
import { PasswordService } from "../../utils/services/password.service";
|
import { PasswordService } from "../../utils/services/password.service";
|
||||||
@@ -15,17 +16,25 @@ export class AuthService {
|
|||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly tokensService: TokensService,
|
private readonly tokensService: TokensService,
|
||||||
private readonly passwordService: PasswordService,
|
private readonly passwordService: PasswordService,
|
||||||
|
private readonly twoFactorService: TwoFactorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
//==============================================
|
//==============================================
|
||||||
async login(loginDto: LoginDto) {
|
async login(loginDto: LoginDto) {
|
||||||
const { email, password } = loginDto;
|
const { email, password, twoFactorToken } = loginDto;
|
||||||
|
|
||||||
const user = await this.checkUserLoginCredentialWithEmail(email, password);
|
const user = await this.checkUserLoginCredentialWithEmail(email, password);
|
||||||
|
|
||||||
if (!user.isActive) throw new BadRequestException(AuthMessage.USER_ACCOUNT_INACTIVE);
|
if (!user.isActive) throw new BadRequestException(AuthMessage.USER_ACCOUNT_INACTIVE);
|
||||||
|
|
||||||
|
if (user.is2FAEnabled) {
|
||||||
|
if (!twoFactorToken) throw new BadRequestException(TwoFactorMessage.TWO_FACTOR_TOKEN_REQUIRED);
|
||||||
|
|
||||||
|
const is2FAValid = await this.twoFactorService.validateTwoFactorToken(user, twoFactorToken);
|
||||||
|
if (!is2FAValid) throw new BadRequestException(TwoFactorMessage.TWO_FACTOR_TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
const tokens = await this.tokensService.generateTokens(user);
|
const tokens = await this.tokensService.generateTokens(user);
|
||||||
|
|
||||||
await this.notificationQueue.addLoginNotification(user.id, {
|
await this.notificationQueue.addLoginNotification(user.id, {
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { EntityManager } from "@mikro-orm/postgresql";
|
||||||
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { TwoFactorMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
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, Setup2FADto, UseBackupCodeDto, Verify2FADto } from "../DTO/create-two-factor.dto";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TwoFactorService {
|
||||||
|
constructor(
|
||||||
|
private readonly mailServerService: MailServerService,
|
||||||
|
private readonly userRepository: UserRepository,
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
//==============================================
|
||||||
|
async setup2FA(wildduckUserId: string, setup2FADto: Setup2FADto) {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.mailServerService.twoFactor.setupTOTP(wildduckUserId, {
|
||||||
|
issuer: setup2FADto.issuer || "DMail Service",
|
||||||
|
fresh: setup2FADto.fresh || false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TwoFactorMessage.TWO_FACTOR_SETUP_INITIATED,
|
||||||
|
qr: response.qrcode,
|
||||||
|
secret: response.seed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================
|
||||||
|
async enable2FA(wildduckUserId: string, enable2FADto: Enable2FADto) {
|
||||||
|
const user = await this.userRepository.findOne({ wildduckUserId });
|
||||||
|
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
|
await firstValueFrom(
|
||||||
|
this.mailServerService.twoFactor.enableTOTP(wildduckUserId, {
|
||||||
|
token: enable2FADto.token,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
user.is2FAEnabled = true;
|
||||||
|
user.twoFactorEnabledAt = new Date();
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TwoFactorMessage.TWO_FACTOR_ENABLED_SUCCESSFULLY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================
|
||||||
|
async verify2FA(wildduckUserId: string, verify2FADto: Verify2FADto) {
|
||||||
|
await firstValueFrom(
|
||||||
|
this.mailServerService.twoFactor.checkTOTP(wildduckUserId, {
|
||||||
|
token: verify2FADto.token,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TwoFactorMessage.TWO_FACTOR_TOKEN_VERIFIED_SUCCESSFULLY,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================
|
||||||
|
async get2FAStatus(wildduckUserId: string) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
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 firstValueFrom(this.mailServerService.twoFactor.disableAll2FA(wildduckUserId));
|
||||||
|
|
||||||
|
user.is2FAEnabled = false;
|
||||||
|
user.twoFactorEnabledAt = undefined;
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TwoFactorMessage.TWO_FACTOR_DISABLED_SUCCESSFULLY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================
|
||||||
|
async generateBackupCodes(wildduckUserId: string) {
|
||||||
|
const user = await this.userRepository.findOne({ wildduckUserId });
|
||||||
|
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
|
if (!user.is2FAEnabled) throw new BadRequestException(TwoFactorMessage.TWO_FACTOR_NOT_ENABLED);
|
||||||
|
|
||||||
|
const response = await firstValueFrom(this.mailServerService.twoFactor.generateBackupCodes(user.wildduckUserId));
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TwoFactorMessage.TWO_FACTOR_BACKUP_CODES_GENERATED_SUCCESSFULLY,
|
||||||
|
backupCodes: response.codes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================
|
||||||
|
async useBackupCode(wildduckUserId: string, useBackupCodeDto: UseBackupCodeDto) {
|
||||||
|
const user = await this.userRepository.findOne({ wildduckUserId });
|
||||||
|
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
|
if (!user.is2FAEnabled) throw new BadRequestException(TwoFactorMessage.TWO_FACTOR_NOT_ENABLED);
|
||||||
|
|
||||||
|
await firstValueFrom(
|
||||||
|
this.mailServerService.twoFactor.useBackupCode(wildduckUserId, {
|
||||||
|
code: useBackupCodeDto.code,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: TwoFactorMessage.TWO_FACTOR_BACKUP_CODE_VERIFIED_SUCCESSFULLY,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================
|
||||||
|
async validateTwoFactorToken(user: User, token: string): Promise<boolean> {
|
||||||
|
if (!user.is2FAEnabled) return true;
|
||||||
|
|
||||||
|
const totpResult = await this.verify2FA(user.wildduckUserId, { token });
|
||||||
|
if (totpResult.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupResult = await this.useBackupCode(user.wildduckUserId, { code: token });
|
||||||
|
return backupResult.valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,18 +8,6 @@ import { Domain } from "../../domains/entities/domain.entity";
|
|||||||
import { Template } from "../../templates/entities/template.entity";
|
import { Template } from "../../templates/entities/template.entity";
|
||||||
import { UserRepository } from "../repositories/user.repository";
|
import { UserRepository } from "../repositories/user.repository";
|
||||||
|
|
||||||
export interface WebPushSubscriptionData {
|
|
||||||
endpoint: string;
|
|
||||||
keys: {
|
|
||||||
p256dh: string;
|
|
||||||
auth: string;
|
|
||||||
};
|
|
||||||
deviceType?: string;
|
|
||||||
deviceName?: string;
|
|
||||||
registeredAt?: Date;
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity({ repository: () => UserRepository })
|
@Entity({ repository: () => UserRepository })
|
||||||
@Unique({ properties: ["userName", "domain"], name: "unique_user_name_domain", options: { where: "deleted_at is null" } })
|
@Unique({ properties: ["userName", "domain"], name: "unique_user_name_domain", options: { where: "deleted_at is null" } })
|
||||||
export class User extends BaseEntity {
|
export class User extends BaseEntity {
|
||||||
@@ -63,15 +51,12 @@ export class User extends BaseEntity {
|
|||||||
@Property({ default: true, nullable: false })
|
@Property({ default: true, nullable: false })
|
||||||
isActive: boolean & Opt;
|
isActive: boolean & Opt;
|
||||||
|
|
||||||
// Push notification fields - now stores web push subscription objects
|
// Two-Factor Authentication fields
|
||||||
@Property({ type: "json", nullable: true })
|
@Property({ type: "boolean", default: false })
|
||||||
pushTokens?: WebPushSubscriptionData[];
|
is2FAEnabled: boolean & Opt;
|
||||||
|
|
||||||
@Property({ type: "boolean", default: true })
|
|
||||||
pushNotificationsEnabled: boolean & Opt;
|
|
||||||
|
|
||||||
@Property({ type: "timestamptz", nullable: true })
|
@Property({ type: "timestamptz", nullable: true })
|
||||||
lastPushNotificationAt?: Date;
|
twoFactorEnabledAt?: Date;
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user