243 lines
7.0 KiB
TypeScript
243 lines
7.0 KiB
TypeScript
import axios from 'axios'
|
||
import { prisma } from '@/lib/prisma'
|
||
|
||
export interface SmsResponse {
|
||
success: boolean
|
||
message: string
|
||
}
|
||
|
||
export interface SmsVerificationResponse {
|
||
success: boolean
|
||
message: string
|
||
}
|
||
|
||
export class SmsService {
|
||
private email: string
|
||
private apiKey: string
|
||
private isDevelopment: boolean
|
||
|
||
constructor() {
|
||
this.email = process.env.SMS_AERO_EMAIL!
|
||
this.apiKey = process.env.SMS_AERO_API_KEY!
|
||
this.isDevelopment = process.env.NODE_ENV === 'development' || process.env.SMS_DEV_MODE === 'true'
|
||
|
||
if (!this.isDevelopment && (!this.email || !this.apiKey)) {
|
||
throw new Error('SMS Aero credentials not configured')
|
||
}
|
||
}
|
||
|
||
private generateSmsCode(): string {
|
||
if (this.isDevelopment) {
|
||
return '1234'
|
||
}
|
||
return Math.floor(1000 + Math.random() * 9000).toString()
|
||
}
|
||
|
||
private validatePhoneNumber(phone: string): boolean {
|
||
const phoneRegex = /^7\d{10}$/
|
||
return phoneRegex.test(phone)
|
||
}
|
||
|
||
private formatPhoneNumber(phone: string): string {
|
||
// Убираем все символы кроме цифр
|
||
const cleanPhone = phone.replace(/\D/g, '')
|
||
|
||
// Если номер начинается с 8, заменяем на 7
|
||
if (cleanPhone.startsWith('8')) {
|
||
return '7' + cleanPhone.slice(1)
|
||
}
|
||
|
||
// Если номер начинается с +7, убираем +
|
||
if (cleanPhone.startsWith('7')) {
|
||
return cleanPhone
|
||
}
|
||
|
||
// Если номер без кода страны, добавляем 7
|
||
if (cleanPhone.length === 10) {
|
||
return '7' + cleanPhone
|
||
}
|
||
|
||
return cleanPhone
|
||
}
|
||
|
||
async sendSmsCode(phone: string): Promise<SmsResponse> {
|
||
try {
|
||
const formattedPhone = this.formatPhoneNumber(phone)
|
||
|
||
if (!this.validatePhoneNumber(formattedPhone)) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверный формат номера телефона'
|
||
}
|
||
}
|
||
|
||
const code = this.generateSmsCode()
|
||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000) // 5 минут
|
||
|
||
// Удаляем старые коды для этого номера
|
||
await prisma.smsCode.deleteMany({
|
||
where: { phone: formattedPhone }
|
||
})
|
||
|
||
// Сохраняем код в базе данных
|
||
await prisma.smsCode.create({
|
||
data: {
|
||
code,
|
||
phone: formattedPhone,
|
||
expiresAt,
|
||
attempts: 0,
|
||
maxAttempts: 3
|
||
}
|
||
})
|
||
|
||
// В режиме разработки не отправляем SMS
|
||
if (this.isDevelopment) {
|
||
console.log(`Development mode: SMS code ${code} for phone ${formattedPhone}`)
|
||
return {
|
||
success: true,
|
||
message: 'SMS код отправлен успешно (режим разработки)'
|
||
}
|
||
}
|
||
|
||
// Отправляем SMS через SMS Aero API с HTTP Basic Auth
|
||
const response = await axios.get(
|
||
`https://gate.smsaero.ru/v2/sms/send`,
|
||
{
|
||
params: {
|
||
number: formattedPhone,
|
||
text: `Код подтверждения SferaV: ${code}`,
|
||
sign: 'SMS Aero'
|
||
},
|
||
auth: {
|
||
username: this.email,
|
||
password: this.apiKey
|
||
},
|
||
headers: {
|
||
'Accept': 'application/json'
|
||
}
|
||
}
|
||
)
|
||
|
||
console.log('SMS Aero response:', response.data)
|
||
|
||
if (response.data.success) {
|
||
return {
|
||
success: true,
|
||
message: 'SMS код отправлен успешно'
|
||
}
|
||
} else {
|
||
console.error('SMS Aero API error:', response.data)
|
||
return {
|
||
success: false,
|
||
message: response.data.message || 'Ошибка при отправке SMS'
|
||
}
|
||
}
|
||
|
||
} catch (error: unknown) {
|
||
console.error('Error sending SMS:', error)
|
||
|
||
// Детальная информация об ошибке
|
||
if (axios.isAxiosError(error)) {
|
||
console.error('Response status:', error.response?.status)
|
||
console.error('Response data:', error.response?.data)
|
||
|
||
if (error.response?.status === 401) {
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка авторизации SMS API. Проверьте настройки.'
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке SMS'
|
||
}
|
||
}
|
||
}
|
||
|
||
async verifySmsCode(phone: string, code: string): Promise<SmsVerificationResponse> {
|
||
try {
|
||
const formattedPhone = this.formatPhoneNumber(phone)
|
||
|
||
if (!this.validatePhoneNumber(formattedPhone)) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверный формат номера телефона'
|
||
}
|
||
}
|
||
|
||
// Ищем активный код для этого номера
|
||
const smsCode = await prisma.smsCode.findFirst({
|
||
where: {
|
||
phone: formattedPhone,
|
||
isUsed: false,
|
||
expiresAt: {
|
||
gte: new Date()
|
||
}
|
||
},
|
||
orderBy: {
|
||
createdAt: 'desc'
|
||
}
|
||
})
|
||
|
||
if (!smsCode) {
|
||
return {
|
||
success: false,
|
||
message: 'Код не найден или истек'
|
||
}
|
||
}
|
||
|
||
// Проверяем количество попыток
|
||
if (smsCode.attempts >= smsCode.maxAttempts) {
|
||
// Помечаем код как использованный при превышении лимита попыток
|
||
await prisma.smsCode.update({
|
||
where: { id: smsCode.id },
|
||
data: { isUsed: true }
|
||
})
|
||
|
||
return {
|
||
success: false,
|
||
message: 'Превышено количество попыток ввода кода'
|
||
}
|
||
}
|
||
|
||
// Проверяем правильность кода
|
||
if (smsCode.code !== code) {
|
||
// Увеличиваем счетчик попыток при неправильном коде
|
||
await prisma.smsCode.update({
|
||
where: { id: smsCode.id },
|
||
data: { attempts: smsCode.attempts + 1 }
|
||
})
|
||
|
||
const remainingAttempts = smsCode.maxAttempts - smsCode.attempts - 1
|
||
return {
|
||
success: false,
|
||
message: remainingAttempts > 0
|
||
? `Неверный код. Осталось попыток: ${remainingAttempts}`
|
||
: 'Неверный код. Превышено количество попыток'
|
||
}
|
||
}
|
||
|
||
// Код правильный - помечаем как использованный
|
||
await prisma.smsCode.update({
|
||
where: { id: smsCode.id },
|
||
data: { isUsed: true }
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Код подтвержден успешно'
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error verifying SMS code:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при проверке кода'
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
}
|