Files
prosapp-migration/backend/src/auth/email-otp.service.ts
T

103 lines
3.9 KiB
TypeScript

import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { PrismaService } from '../prisma/prisma.service';
const OTP_TTL_MS = 10 * 60 * 1000; // 10 minutos
@Injectable()
export class EmailOtpService {
private readonly logger = new Logger(EmailOtpService.name);
private readonly otps = new Map<string, { code: string; expiresAt: number }>();
constructor(private readonly prisma: PrismaService) {}
private async createTransport() {
// Prioridad: DB > env vars
const dbConfig = await this.prisma.settings.findUnique({ where: { key: 'smtp_config' } })
.then(r => r?.value as any).catch(() => null);
const host = dbConfig?.host || process.env.SMTP_HOST;
const port = parseInt(dbConfig?.port || process.env.SMTP_PORT || '587');
const user = dbConfig?.user || process.env.SMTP_USER;
const pass = dbConfig?.pass || process.env.SMTP_PASS;
if (!host || !user || !pass) {
throw new BadRequestException(
'Correo no configurado. Configure el SMTP en Ajustes > Email.',
);
}
return nodemailer.createTransport({
host,
port,
secure: port === 465,
auth: { user, pass },
});
}
async getSmtpConfig() {
return this.prisma.settings.findUnique({ where: { key: 'smtp_config' } })
.then(r => (r?.value as any) ?? null);
}
async saveSmtpConfig(config: { host: string; port: string; user: string; pass: string; from?: string }) {
await this.prisma.settings.upsert({
where: { key: 'smtp_config' },
create: { key: 'smtp_config', value: config as any },
update: { value: config as any },
});
return { message: 'Configuración SMTP guardada' };
}
async sendOtp(email: string): Promise<void> {
const code = Math.floor(100000 + Math.random() * 900000).toString();
this.otps.set(email.toLowerCase(), { code, expiresAt: Date.now() + OTP_TTL_MS });
const dbConfig = await this.prisma.settings.findUnique({ where: { key: 'smtp_config' } })
.then(r => r?.value as any).catch(() => null);
const from = dbConfig?.from || process.env.EMAIL_FROM || process.env.SMTP_USER;
const transporter = await this.createTransport();
try {
await transporter.sendMail({
from: `ProsApp <${from}>`,
to: email,
subject: 'Código de verificación ProsApp',
html: `
<div style="font-family:sans-serif;max-width:480px;margin:auto;padding:32px">
<img src="https://prosapp.co/img/logo_prosapp.png" height="40" style="margin-bottom:24px">
<h2 style="color:#1e293b;margin:0 0 8px">Verifica tu correo</h2>
<p style="color:#64748b;margin:0 0 24px">Usa este código para vincular tu correo a ProsApp. Válido por 10 minutos.</p>
<div style="background:#f0f8ff;border:1px solid #bae3ff;border-radius:12px;padding:24px;text-align:center">
<span style="font-size:36px;font-weight:700;letter-spacing:10px;color:#42A4EF">${code}</span>
</div>
<p style="color:#94a3b8;font-size:12px;margin-top:24px">Si no solicitaste esto, ignora este correo.</p>
</div>
`,
});
this.logger.log(`Email OTP enviado a ${email}`);
await this.prisma.message_logs.create({
data: { channel: 'email', recipient: email, body: `OTP: ${code}`, status: 'sent' },
}).catch(() => {});
} catch (err) {
await this.prisma.message_logs.create({
data: { channel: 'email', recipient: email, body: 'OTP', status: 'error', error: String(err) },
}).catch(() => {});
throw err;
}
}
verifyOtp(email: string, code: string): boolean {
const key = email.toLowerCase();
const entry = this.otps.get(key);
if (!entry) return false;
if (Date.now() > entry.expiresAt) {
this.otps.delete(key);
return false;
}
if (entry.code !== code) return false;
this.otps.delete(key);
return true;
}
}