ledgerone_backend/write-2fa.mjs
2026-03-14 08:51:16 -04:00

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");