151 lines
5.3 KiB
JavaScript
151 lines
5.3 KiB
JavaScript
import { writeFileSync, mkdirSync } from "fs";
|
|
|
|
mkdirSync("src/auth/twofa", { recursive: true });
|
|
|
|
// ─── two-factor.service.ts ───────────────────────────────────────────────────
|
|
writeFileSync("src/auth/twofa/two-factor.service.ts", `import { BadRequestException, Injectable } from "@nestjs/common";
|
|
import { authenticator } from "otplib";
|
|
import * as QRCode from "qrcode";
|
|
import { PrismaService } from "../../prisma/prisma.service";
|
|
import { EncryptionService } from "../../common/encryption.service";
|
|
|
|
@Injectable()
|
|
export class TwoFactorService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly encryption: EncryptionService,
|
|
) {}
|
|
|
|
async generateSecret(userId: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { email: true, twoFactorEnabled: true },
|
|
});
|
|
if (!user) throw new BadRequestException("User not found.");
|
|
if (user.twoFactorEnabled) {
|
|
throw new BadRequestException("2FA is already enabled.");
|
|
}
|
|
|
|
const secret = authenticator.generateSecret();
|
|
const otpAuthUrl = authenticator.keyuri(user.email, "LedgerOne", secret);
|
|
const qrCodeDataUrl = await QRCode.toDataURL(otpAuthUrl);
|
|
|
|
// Store encrypted secret temporarily (not yet enabled)
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: { twoFactorSecret: this.encryption.encrypt(secret) },
|
|
});
|
|
|
|
return { qrCode: qrCodeDataUrl, otpAuthUrl };
|
|
}
|
|
|
|
async enableTwoFactor(userId: string, token: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { twoFactorSecret: true, twoFactorEnabled: true },
|
|
});
|
|
if (!user?.twoFactorSecret) {
|
|
throw new BadRequestException("Please generate a 2FA secret first.");
|
|
}
|
|
if (user.twoFactorEnabled) {
|
|
throw new BadRequestException("2FA is already enabled.");
|
|
}
|
|
|
|
const secret = this.encryption.decrypt(user.twoFactorSecret);
|
|
const isValid = authenticator.verify({ token, secret });
|
|
if (!isValid) {
|
|
throw new BadRequestException("Invalid TOTP token.");
|
|
}
|
|
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: { twoFactorEnabled: true },
|
|
});
|
|
await this.prisma.auditLog.create({
|
|
data: { userId, action: "auth.2fa.enabled", metadata: {} },
|
|
});
|
|
|
|
return { message: "2FA enabled successfully." };
|
|
}
|
|
|
|
async disableTwoFactor(userId: string, token: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { twoFactorSecret: true, twoFactorEnabled: true },
|
|
});
|
|
if (!user?.twoFactorEnabled || !user.twoFactorSecret) {
|
|
throw new BadRequestException("2FA is not enabled.");
|
|
}
|
|
|
|
const secret = this.encryption.decrypt(user.twoFactorSecret);
|
|
const isValid = authenticator.verify({ token, secret });
|
|
if (!isValid) {
|
|
throw new BadRequestException("Invalid TOTP token.");
|
|
}
|
|
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: { twoFactorEnabled: false, twoFactorSecret: null },
|
|
});
|
|
await this.prisma.auditLog.create({
|
|
data: { userId, action: "auth.2fa.disabled", metadata: {} },
|
|
});
|
|
|
|
return { message: "2FA disabled successfully." };
|
|
}
|
|
|
|
verifyToken(secret: string, token: string): boolean {
|
|
return authenticator.verify({ token, secret });
|
|
}
|
|
|
|
decryptSecret(encryptedSecret: string): string {
|
|
return this.encryption.decrypt(encryptedSecret);
|
|
}
|
|
}
|
|
`);
|
|
|
|
// ─── two-factor.controller.ts ────────────────────────────────────────────────
|
|
writeFileSync("src/auth/twofa/two-factor.controller.ts", `import { Body, Controller, Delete, Post } from "@nestjs/common";
|
|
import { ok } from "../../common/response";
|
|
import { TwoFactorService } from "./two-factor.service";
|
|
import { CurrentUser } from "../../common/decorators/current-user.decorator";
|
|
|
|
@Controller("auth/2fa")
|
|
export class TwoFactorController {
|
|
constructor(private readonly twoFactorService: TwoFactorService) {}
|
|
|
|
@Post("generate")
|
|
async generate(@CurrentUser() userId: string) {
|
|
const data = await this.twoFactorService.generateSecret(userId);
|
|
return ok(data);
|
|
}
|
|
|
|
@Post("enable")
|
|
async enable(@CurrentUser() userId: string, @Body("token") token: string) {
|
|
const data = await this.twoFactorService.enableTwoFactor(userId, token);
|
|
return ok(data);
|
|
}
|
|
|
|
@Delete("disable")
|
|
async disable(@CurrentUser() userId: string, @Body("token") token: string) {
|
|
const data = await this.twoFactorService.disableTwoFactor(userId, token);
|
|
return ok(data);
|
|
}
|
|
}
|
|
`);
|
|
|
|
// ─── two-factor.module.ts ────────────────────────────────────────────────────
|
|
writeFileSync("src/auth/twofa/two-factor.module.ts", `import { Module } from "@nestjs/common";
|
|
import { TwoFactorController } from "./two-factor.controller";
|
|
import { TwoFactorService } from "./two-factor.service";
|
|
|
|
@Module({
|
|
controllers: [TwoFactorController],
|
|
providers: [TwoFactorService],
|
|
exports: [TwoFactorService],
|
|
})
|
|
export class TwoFactorModule {}
|
|
`);
|
|
|
|
console.log("2FA files written");
|