Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен README с описанием функционала и технологий. Реализована анимация и адаптивный дизайн. Настроена авторизация с использованием Apollo Client.
This commit is contained in:
289
src/services/dadata-service.ts
Normal file
289
src/services/dadata-service.ts
Normal file
@ -0,0 +1,289 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export interface DaDataCompany {
|
||||
value: string
|
||||
unrestricted_value: string
|
||||
data: {
|
||||
kpp?: string
|
||||
management?: {
|
||||
name?: string
|
||||
post?: string
|
||||
}
|
||||
hid: string
|
||||
type: string
|
||||
state?: {
|
||||
status?: string
|
||||
actuality_date?: number
|
||||
registration_date?: number
|
||||
liquidation_date?: number
|
||||
}
|
||||
opf?: {
|
||||
code?: string
|
||||
full?: string
|
||||
short?: string
|
||||
}
|
||||
name: {
|
||||
full_with_opf?: string
|
||||
short_with_opf?: string
|
||||
full?: string
|
||||
short?: string
|
||||
}
|
||||
inn: string
|
||||
ogrn?: string
|
||||
ogrn_date?: number
|
||||
okpo?: string
|
||||
okato?: string
|
||||
oktmo?: string
|
||||
okved?: string
|
||||
employee_count?: number
|
||||
phones?: object[]
|
||||
emails?: object[]
|
||||
finance?: {
|
||||
revenue?: number
|
||||
tax_system?: string
|
||||
}
|
||||
address?: {
|
||||
value?: string
|
||||
unrestricted_value?: string
|
||||
data?: {
|
||||
region_with_type?: string
|
||||
city_with_type?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DaDataResponse {
|
||||
suggestions: DaDataCompany[]
|
||||
}
|
||||
|
||||
export interface OrganizationData {
|
||||
inn: string
|
||||
kpp?: string
|
||||
name: string
|
||||
fullName: string
|
||||
address: string
|
||||
addressFull?: string
|
||||
ogrn?: string
|
||||
ogrnDate?: Date
|
||||
isActive: boolean
|
||||
type: 'FULFILLMENT' | 'SELLER'
|
||||
|
||||
// Статус организации
|
||||
status?: string
|
||||
actualityDate?: Date
|
||||
registrationDate?: Date
|
||||
liquidationDate?: Date
|
||||
|
||||
// Руководитель
|
||||
managementName?: string
|
||||
managementPost?: string
|
||||
|
||||
// ОПФ
|
||||
opfCode?: string
|
||||
opfFull?: string
|
||||
opfShort?: string
|
||||
|
||||
// Коды статистики
|
||||
okato?: string
|
||||
oktmo?: string
|
||||
okpo?: string
|
||||
okved?: string
|
||||
|
||||
// Контакты
|
||||
phones?: object[]
|
||||
emails?: object[]
|
||||
|
||||
// Финансовые данные
|
||||
employeeCount?: number
|
||||
revenue?: bigint
|
||||
taxSystem?: string
|
||||
|
||||
rawData: DaDataCompany
|
||||
}
|
||||
|
||||
export class DaDataService {
|
||||
private apiKey: string
|
||||
private apiUrl: string
|
||||
|
||||
constructor() {
|
||||
this.apiKey = process.env.DADATA_API_KEY!
|
||||
this.apiUrl = process.env.DADATA_API_URL!
|
||||
|
||||
if (!this.apiKey || !this.apiUrl) {
|
||||
throw new Error('DaData API credentials not configured')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает информацию об организации по ИНН
|
||||
*/
|
||||
async getOrganizationByInn(inn: string): Promise<OrganizationData | null> {
|
||||
try {
|
||||
const response = await axios.post<DaDataResponse>(
|
||||
`${this.apiUrl}/findById/party`,
|
||||
{
|
||||
query: inn,
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Token ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data?.suggestions?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const company = response.data.suggestions[0]
|
||||
|
||||
// Определяем тип организации на основе ОПФ
|
||||
const organizationType = this.determineOrganizationType(company)
|
||||
|
||||
return {
|
||||
inn: company.data.inn,
|
||||
kpp: company.data.kpp || undefined,
|
||||
name: company.data.name.short || company.data.name.full || 'Название не указано',
|
||||
fullName: company.data.name.full_with_opf || '',
|
||||
address: company.data.address?.value || '',
|
||||
addressFull: company.data.address?.unrestricted_value || undefined,
|
||||
ogrn: company.data.ogrn || undefined,
|
||||
ogrnDate: this.parseDate(company.data.ogrn_date),
|
||||
|
||||
// Статус организации
|
||||
status: company.data.state?.status,
|
||||
actualityDate: this.parseDate(company.data.state?.actuality_date),
|
||||
registrationDate: this.parseDate(company.data.state?.registration_date),
|
||||
liquidationDate: this.parseDate(company.data.state?.liquidation_date),
|
||||
|
||||
// Руководитель
|
||||
managementName: company.data.management?.name,
|
||||
managementPost: company.data.management?.post,
|
||||
|
||||
// ОПФ
|
||||
opfCode: company.data.opf?.code,
|
||||
opfFull: company.data.opf?.full,
|
||||
opfShort: company.data.opf?.short,
|
||||
|
||||
// Коды статистики
|
||||
okato: company.data.okato,
|
||||
oktmo: company.data.oktmo,
|
||||
okpo: company.data.okpo,
|
||||
okved: company.data.okved,
|
||||
|
||||
// Контакты
|
||||
phones: company.data.phones || undefined,
|
||||
emails: company.data.emails || undefined,
|
||||
|
||||
// Финансовые данные
|
||||
employeeCount: company.data.employee_count || undefined,
|
||||
revenue: company.data.finance?.revenue ? BigInt(company.data.finance.revenue) : undefined,
|
||||
taxSystem: company.data.finance?.tax_system || undefined,
|
||||
|
||||
isActive: company.data.state?.status === 'ACTIVE',
|
||||
type: organizationType,
|
||||
rawData: company
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching organization data from DaData:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Безопасно парсит дату из timestamp, возвращает undefined для некорректных дат
|
||||
*/
|
||||
private parseDate(timestamp?: number): Date | undefined {
|
||||
if (!timestamp) return undefined
|
||||
|
||||
try {
|
||||
const date = new Date(timestamp * 1000)
|
||||
// Проверяем, что дата валидна и разумна (между 1900 и 2100 годами)
|
||||
if (isNaN(date.getTime()) || date.getFullYear() < 1900 || date.getFullYear() > 2100) {
|
||||
return undefined
|
||||
}
|
||||
return date
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Определяет тип организации на основе ОПФ (организационно-правовая форма)
|
||||
*/
|
||||
private determineOrganizationType(company: DaDataCompany): 'FULFILLMENT' | 'SELLER' {
|
||||
const opfCode = company.data.opf?.code
|
||||
|
||||
// Индивидуальные предприниматели чаще работают как селлеры
|
||||
if (company.data.type === 'INDIVIDUAL' || opfCode === '50102') {
|
||||
return 'SELLER'
|
||||
}
|
||||
|
||||
// ООО, АО и другие юридические лица чаще работают с фулфилментом
|
||||
return 'FULFILLMENT'
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидирует ИНН по контрольной сумме
|
||||
*/
|
||||
validateInn(inn: string): boolean {
|
||||
const digits = inn.replace(/\D/g, '')
|
||||
|
||||
if (digits.length !== 10 && digits.length !== 12) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем контрольную сумму для 10-значного ИНН (юридические лица)
|
||||
if (digits.length === 10) {
|
||||
const checksum = this.calculateInn10Checksum(digits)
|
||||
return checksum === parseInt(digits[9])
|
||||
}
|
||||
|
||||
// Проверяем контрольную сумму для 12-значного ИНН (ИП)
|
||||
if (digits.length === 12) {
|
||||
const checksum1 = this.calculateInn12Checksum1(digits)
|
||||
const checksum2 = this.calculateInn12Checksum2(digits)
|
||||
return checksum1 === parseInt(digits[10]) && checksum2 === parseInt(digits[11])
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private calculateInn10Checksum(inn: string): number {
|
||||
const weights = [2, 4, 10, 3, 5, 9, 4, 6, 8]
|
||||
let sum = 0
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
sum += parseInt(inn[i]) * weights[i]
|
||||
}
|
||||
|
||||
return sum % 11 % 10
|
||||
}
|
||||
|
||||
private calculateInn12Checksum1(inn: string): number {
|
||||
const weights = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
|
||||
let sum = 0
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
sum += parseInt(inn[i]) * weights[i]
|
||||
}
|
||||
|
||||
return sum % 11 % 10
|
||||
}
|
||||
|
||||
private calculateInn12Checksum2(inn: string): number {
|
||||
const weights = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
|
||||
let sum = 0
|
||||
|
||||
for (let i = 0; i < 11; i++) {
|
||||
sum += parseInt(inn[i]) * weights[i]
|
||||
}
|
||||
|
||||
return sum % 11 % 10
|
||||
}
|
||||
}
|
223
src/services/marketplace-service.ts
Normal file
223
src/services/marketplace-service.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export interface MarketplaceValidationResult {
|
||||
isValid: boolean
|
||||
message: string
|
||||
data?: {
|
||||
sellerId?: string
|
||||
sellerName?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface WildberriesSellerInfo {
|
||||
id: number
|
||||
name: string
|
||||
inn: string
|
||||
kpp?: string
|
||||
}
|
||||
|
||||
export interface OzonSellerInfo {
|
||||
id: number
|
||||
name: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export class MarketplaceService {
|
||||
private wbApiUrl: string
|
||||
private ozonApiUrl: string
|
||||
|
||||
constructor() {
|
||||
this.wbApiUrl = process.env.WILDBERRIES_API_URL || 'https://common-api.wildberries.ru'
|
||||
this.ozonApiUrl = process.env.OZON_API_URL || 'https://api-seller.ozon.ru'
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидирует API ключ Wildberries
|
||||
*/
|
||||
async validateWildberriesApiKey(apiKey: string): Promise<MarketplaceValidationResult> {
|
||||
try {
|
||||
// Пытаемся получить информацию о продавце
|
||||
const response = await axios.get(
|
||||
`${this.wbApiUrl}/api/v1/seller-info`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
const sellerData = response.data
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
message: 'API ключ Wildberries валиден',
|
||||
data: {
|
||||
sellerId: sellerData.id?.toString(),
|
||||
sellerName: sellerData.name || sellerData.supplierName,
|
||||
inn: sellerData.inn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Не удалось получить информацию о продавце Wildberries'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Wildberries API validation error:', error)
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Неверный API ключ Wildberries'
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response?.status === 403) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Доступ запрещён. Проверьте права API ключа Wildberries'
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Превышено время ожидания ответа от Wildberries API'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Ошибка при проверке API ключа Wildberries'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидирует API ключ Ozon
|
||||
*/
|
||||
async validateOzonApiKey(apiKey: string, clientId?: string): Promise<MarketplaceValidationResult> {
|
||||
try {
|
||||
// Для Ozon нужен Client-Id
|
||||
if (!clientId) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Для Ozon API требуется Client-Id'
|
||||
}
|
||||
}
|
||||
|
||||
// Пытаемся получить информацию о продавце
|
||||
const response = await axios.post(
|
||||
`${this.ozonApiUrl}/v1/seller/info`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Api-Key': apiKey,
|
||||
'Client-Id': clientId,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status === 200 && response.data?.result) {
|
||||
const sellerData = response.data.result as OzonSellerInfo
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
message: 'API ключ Ozon валиден',
|
||||
data: {
|
||||
sellerId: sellerData.id?.toString(),
|
||||
sellerName: sellerData.name,
|
||||
status: sellerData.status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Не удалось получить информацию о продавце Ozon'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ozon API validation error:', error)
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Неверный API ключ или Client-Id для Ozon'
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response?.status === 403) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Доступ запрещён. Проверьте права API ключа Ozon'
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Превышено время ожидания ответа от Ozon API'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Ошибка при проверке API ключа Ozon'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Общий метод валидации API ключа по типу маркетплейса
|
||||
*/
|
||||
async validateApiKey(
|
||||
marketplace: 'WILDBERRIES' | 'OZON',
|
||||
apiKey: string,
|
||||
clientId?: string
|
||||
): Promise<MarketplaceValidationResult> {
|
||||
switch (marketplace) {
|
||||
case 'WILDBERRIES':
|
||||
return this.validateWildberriesApiKey(apiKey)
|
||||
case 'OZON':
|
||||
return this.validateOzonApiKey(apiKey, clientId)
|
||||
default:
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Неподдерживаемый тип маркетплейса'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет формат API ключа перед отправкой запроса
|
||||
*/
|
||||
validateApiKeyFormat(marketplace: 'WILDBERRIES' | 'OZON', apiKey: string): boolean {
|
||||
if (!apiKey || typeof apiKey !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (marketplace) {
|
||||
case 'WILDBERRIES':
|
||||
// Wildberries API ключи обычно содержат буквы, цифры и дефисы
|
||||
return /^[a-zA-Z0-9\-_]{10,}$/.test(apiKey)
|
||||
case 'OZON':
|
||||
// Ozon API ключи обычно содержат буквы, цифры и дефисы
|
||||
return /^[a-zA-Z0-9\-_]{10,}$/.test(apiKey)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
78
src/services/s3-service.ts
Normal file
78
src/services/s3-service.ts
Normal file
@ -0,0 +1,78 @@
|
||||
interface S3Config {
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
region: string
|
||||
endpoint: string
|
||||
bucket: string
|
||||
}
|
||||
|
||||
const s3Config: S3Config = {
|
||||
accessKeyId: 'I6XD2OR7YO2ZN6L6Z629',
|
||||
secretAccessKey: '9xCOoafisG0aB9lJNvdLO1UuK73fBvMcpHMdijrJ',
|
||||
region: 'ru-1',
|
||||
endpoint: 'https://s3.twcstorage.ru',
|
||||
bucket: '617774af-sfera'
|
||||
}
|
||||
|
||||
export class S3Service {
|
||||
private static async createSignedUrl(fileName: string, fileType: string): Promise<string> {
|
||||
// Для простоты пока используем прямую загрузку через fetch
|
||||
// В продакшене лучше генерировать signed URLs на backend
|
||||
const timestamp = Date.now()
|
||||
const key = `avatars/${timestamp}-${fileName}`
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
static async uploadAvatar(file: File, userId: string): Promise<string> {
|
||||
const fileName = `${userId}-${Date.now()}.${file.name.split('.').pop()}`
|
||||
const key = `avatars/${fileName}`
|
||||
|
||||
try {
|
||||
// Создаем FormData для загрузки
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('key', key)
|
||||
formData.append('bucket', s3Config.bucket)
|
||||
|
||||
// Пока используем простую загрузку через наш API
|
||||
// Позже можно будет сделать прямую загрузку в S3
|
||||
const response = await fetch('/api/upload-avatar', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to upload avatar')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.url
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading avatar:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
static getAvatarUrl(key: string): string {
|
||||
return `${s3Config.endpoint}/${s3Config.bucket}/${key}`
|
||||
}
|
||||
|
||||
static async deleteAvatar(key: string): Promise<void> {
|
||||
try {
|
||||
await fetch('/api/delete-avatar', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ key })
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error deleting avatar:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default S3Service
|
243
src/services/sms-service.ts
Normal file
243
src/services/sms-service.ts
Normal 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: 'Ошибка при проверке кода'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
Reference in New Issue
Block a user