Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен README с описанием функционала и технологий. Реализована анимация и адаптивный дизайн. Настроена авторизация с использованием Apollo Client.

This commit is contained in:
Bivekich
2025-07-16 18:00:41 +03:00
parent d260749bc9
commit 823ef9a28c
69 changed files with 15539 additions and 210 deletions

243
src/services/sms-service.ts Normal file
View File

@ -0,0 +1,243 @@
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: 'Ошибка при проверке кода'
}
}
}
}