From 1461d16983b2bbe9df231065c852d1f40bb8f67e Mon Sep 17 00:00:00 2001 From: Sergey Bolshakov Date: Tue, 10 Mar 2026 21:27:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=81=20sms.ru?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 12 +++++- src/auth/auth.module.ts | 2 + src/auth/sms.service.ts | 82 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index a68cbec..297533d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { YachtsModule } from './yachts/yachts.module'; @@ -10,7 +11,16 @@ import { ReviewsModule } from './reviews/reviews.module'; import { ReservationsModule } from './reservations/reservations.module'; @Module({ - imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule, ReviewsModule, ReservationsModule], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + YachtsModule, + CatalogModule, + AuthModule, + UsersModule, + FilesModule, + ReviewsModule, + ReservationsModule, + ], controllers: [AppController], providers: [AppService], }) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index ab60a3d..f9d1f06 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { UsersModule } from '../users/users.module'; @@ -10,6 +11,7 @@ import { RefreshTokenStoreService } from './refresh-token-store.service'; @Module({ imports: [ + ConfigModule, UsersModule, JwtModule.register({ secret: jwtConstants.secret, diff --git a/src/auth/sms.service.ts b/src/auth/sms.service.ts index 5501908..579383f 100644 --- a/src/auth/sms.service.ts +++ b/src/auth/sms.service.ts @@ -1,7 +1,26 @@ import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; export const SMS_SERVICE = 'SmsService'; +const SMS_RU_SEND_URL = 'https://sms.ru/sms/send'; + +/** Ответ API sms.ru (фрагмент для одного номера). */ +interface SmsRuSmsItem { + status: string; + status_code: number; + sms_id?: string; + status_text?: string; +} + +/** Ответ API sms.ru при json=1. */ +interface SmsRuResponse { + status: string; + status_code: number; + sms?: Record; + balance?: number; +} + /** Интерфейс для отправки SMS. В проде подставьте реализацию (SMS.ru, Twilio и т.д.). */ export interface ISmsSender { sendVerificationCode(phone: string, code: string): Promise; @@ -10,10 +29,67 @@ export interface ISmsSender { @Injectable() export class SmsService implements ISmsSender { private readonly logger = new Logger(SmsService.name); + private readonly apiId: string | undefined; + private readonly isDev: boolean; + + constructor(private readonly configService: ConfigService) { + this.apiId = this.configService.get('SMS_RU_API_ID'); + this.isDev = this.configService.get('NODE_ENV') === 'development'; + if (!this.apiId && !this.isDev) { + this.logger.warn( + 'SMS_RU_API_ID не задан — SMS не отправляются, код верификации только логируется', + ); + } + } async sendVerificationCode(phone: string, code: string): Promise { - // Заглушка: в разработке только логируем. Подключите реальный провайдер SMS. - this.logger.log(`[SMS] Код для ${phone}: ${code}`); - // await this.smsProvider.send(phone, `Ваш код: ${code}`); + if (this.isDev) { + this.logger.log(`[dev] Код верификации для ${phone}: ${code}`); + return; + } + + const msg = `Ваш код: ${code}`; + const to = this.toSmsRuPhone(phone); + + if (!this.apiId) { + this.logger.log(`[SMS stub] Код для ${phone}: ${code}`); + return; + } + + const url = new URL(SMS_RU_SEND_URL); + url.searchParams.set('api_id', this.apiId); + url.searchParams.set('to', to); + url.searchParams.set('msg', msg); + url.searchParams.set('json', '1'); + + try { + const res = await fetch(url.toString()); + const data = (await res.json()) as SmsRuResponse; + + if (data.status !== 'OK' || data.status_code !== 100) { + this.logger.error(`SMS.ru: ошибка запроса status=${data.status} status_code=${data.status_code}`); + throw new Error(`SMS.ru: ${data.status_code}`); + } + + const item = data.sms?.[to]; + if (item?.status !== 'OK') { + const text = item?.status_text ?? 'неизвестная ошибка'; + this.logger.error(`SMS.ru: не удалось отправить на ${to}: ${text}`); + throw new Error(`SMS.ru: ${text}`); + } + + this.logger.log(`SMS отправлен на ${phone}, код=${code}, sms_id=${item.sms_id ?? '—'}`); + } catch (err) { + this.logger.error(`Ошибка отправки SMS на ${phone}:`, err); + throw err; + } + } + + /** Номер в формате sms.ru: только цифры, например 79255070602. */ + private toSmsRuPhone(phone: string): string { + const digits = phone.replace(/\D/g, ''); + if (digits.length === 10 && digits.startsWith('9')) return '7' + digits; + if (digits.length === 11 && digits.startsWith('7')) return digits; + return digits; } }