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(); 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 { 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: `

Verifica tu correo

Usa este código para vincular tu correo a ProsApp. Válido por 10 minutos.

${code}

Si no solicitaste esto, ignora este correo.

`, }); 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; } }