From 1c2f0ca71a2535e00e8e8e8a8fccb8987d1fd9f9 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:26:57 -0500 Subject: [PATCH] fix: strip leading + from phone number before sending to SMS provider Co-Authored-By: Claude Sonnet 4.6 --- backend/src/sms/sms.service.ts | 71 ++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 backend/src/sms/sms.service.ts diff --git a/backend/src/sms/sms.service.ts b/backend/src/sms/sms.service.ts new file mode 100644 index 0000000..61a0501 --- /dev/null +++ b/backend/src/sms/sms.service.ts @@ -0,0 +1,71 @@ +import { Injectable, Logger, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +const SMS_ENDPOINT = 'https://admin.u-site.app/api/sms/send'; +const OTP_TTL_MS = 5 * 60 * 1000; + +@Injectable() +export class SmsService { + private readonly logger = new Logger(SmsService.name); + private readonly otps = new Map(); + + constructor(private prisma: PrismaService) {} + + async getConfig(): Promise<{ api_key: string } | null> { + const setting = await this.prisma.settings.findUnique({ where: { key: 'sms_config' } }); + return (setting?.value as { api_key: string }) ?? null; + } + + async saveConfig(api_key: string): Promise { + await this.prisma.settings.upsert({ + where: { key: 'sms_config' }, + create: { key: 'sms_config', value: { api_key } }, + update: { value: { api_key } }, + }); + } + + async send(numero: string, mensaje: string): Promise<{ ok: boolean; id?: string }> { + const config = await this.getConfig(); + if (!config?.api_key) throw new BadRequestException('SMS API key no configurada'); + + // El proveedor espera el número sin + (ej: 573001234567) + const normalizedNumero = numero.startsWith('+') ? numero.slice(1) : numero; + + const res = await fetch(SMS_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${config.api_key}`, + }, + body: JSON.stringify({ numero: normalizedNumero, mensaje }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + this.logger.error(`SMS error ${res.status}: ${JSON.stringify(err)}`); + throw new BadRequestException(err.error || `Error al enviar SMS: ${res.status}`); + } + + const result = await res.json(); + this.logger.log(`SMS enviado a ${numero}`); + return result; + } + + async sendOtp(phone: string): Promise { + const code = Math.floor(100000 + Math.random() * 900000).toString(); + this.otps.set(phone, { code, expiresAt: Date.now() + OTP_TTL_MS }); + await this.send(phone, `Tu código de verificación ProsApp es: ${code}. Válido por 5 minutos.`); + } + + verifyOtp(phone: string, code: string): boolean { + const entry = this.otps.get(phone); + if (!entry) return false; + if (Date.now() > entry.expiresAt) { + this.otps.delete(phone); + return false; + } + if (entry.code !== code) return false; + this.otps.delete(phone); + return true; + } +}