feat: SMS verification через n8n webhook
- Перенесена проверка SMS кода в n8n webhook (N8N_SMS_VERIFY_WEBHOOK) - Упрощен формат ответа: убран токен, только success/message - sms-verify.php теперь проксирует запросы на n8n - Обновлен JS код: убрано использование токена - Обновлена документация с упрощенным форматом ответа - Протестировано: верный и неверный коды работают корректно
This commit is contained in:
261
docs/n8n_migration_plan.md
Normal file
261
docs/n8n_migration_plan.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# План миграции SMS верификации в n8n
|
||||
|
||||
## Текущая логика (PHP)
|
||||
|
||||
### 1. Отправка SMS (`sms-verify.php?action=send`)
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Frontend │
|
||||
│ (JS) │
|
||||
└──────┬──────┘
|
||||
│ POST: phonenumber
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ sms-verify.php │
|
||||
│ action=send │
|
||||
└──────┬──────────────┘
|
||||
│
|
||||
├─► Нормализация номера (clear_phone)
|
||||
├─► Проверка rate limit (Redis)
|
||||
├─► Генерация кода (generateCode)
|
||||
├─► Сохранение в Redis (setex, 10 мин)
|
||||
├─► Отправка SMS → n8n webhook
|
||||
└─► Ответ: success
|
||||
```
|
||||
|
||||
### 2. Проверка кода (`sms-verify.php?action=verify`)
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Frontend │
|
||||
│ (JS) │
|
||||
└──────┬──────┘
|
||||
│ POST: phonenumber, code
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ sms-verify.php │
|
||||
│ action=verify │
|
||||
└──────┬──────────────┘
|
||||
│
|
||||
├─► Нормализация номера
|
||||
├─► Проверка rate limit попыток
|
||||
├─► Чтение кода из Redis
|
||||
├─► Сравнение кодов
|
||||
├─► Удаление кода из Redis
|
||||
├─► Создание токена верификации
|
||||
└─► Ответ: success, token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Новая логика (n8n)
|
||||
|
||||
### Workflow 1: Отправка SMS
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Webhook │ POST /webhook/sms-send
|
||||
│ Trigger │ Body: { phonenumber: "+79262306381" }
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Function │ Нормализация номера
|
||||
│ (JS Code) │ phone.replace(/[() -+]/g, '').replace(/^(\+?7|8)/, '')
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ GET sms:ratelimit:send:9262306381
|
||||
│ (Get) │ Если >= 5 → ошибка
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Function │ Генерация кода
|
||||
│ (JS Code) │ Math.floor(100000 + Math.random() * 900000)
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ SETEX sms:code:9262306381 600 "106574"
|
||||
│ (Set) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ INCR sms:ratelimit:send:9262306381
|
||||
│ (Increment) │ EXPIRE 3600
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ HTTP Request │ POST к SMS API
|
||||
│ (SMS Send) │ Body: { phone, text: "Код: 106574" }
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ IF (error) │ Если ошибка → удалить код из Redis
|
||||
│ (Error Handle) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Respond to │ { success: true, message: "..." }
|
||||
│ Webhook │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Workflow 2: Проверка кода
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Webhook │ POST /webhook/sms-verify
|
||||
│ Trigger │ Body: { phonenumber: "+79262306381", code: "106574" }
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Function │ Нормализация номера
|
||||
│ (JS Code) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ GET sms:ratelimit:attempts:9262306381
|
||||
│ (Get) │ Если >= 10 → ошибка
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ INCR sms:ratelimit:attempts:9262306381
|
||||
│ (Increment) │ EXPIRE 900
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ GET sms:code:9262306381
|
||||
│ (Get) │ Если null → ошибка "Код не найден"
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ IF │ Если code !== stored_code → ошибка "Неверный код"
|
||||
│ (Compare) │
|
||||
└────────┬────────┘
|
||||
│ (success)
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ DEL sms:code:9262306381
|
||||
│ (Delete) │ DEL sms:ratelimit:attempts:9262306381
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Function │ Генерация токена
|
||||
│ (JS Code) │ crypto.randomBytes(32).toString('hex')
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │ SETEX sms:verified:9262306381 3600 "token"
|
||||
│ (Set) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Respond to │ { success: true, token: "..." }
|
||||
│ Webhook │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Пошаговая миграция
|
||||
|
||||
### Шаг 1: Создать workflows в n8n
|
||||
|
||||
1. Зайти в n8n: https://n8n.clientright.pro
|
||||
2. Создать новый workflow "SMS Send"
|
||||
3. Создать новый workflow "SMS Verify"
|
||||
4. Создать новый workflow "SMS Check Verified" (опционально)
|
||||
|
||||
### Шаг 2: Настроить Redis в n8n
|
||||
|
||||
- Добавить Redis credentials в n8n
|
||||
- Host: `crm.clientright.ru`
|
||||
- Port: `6379`
|
||||
- Password: (из .env)
|
||||
|
||||
### Шаг 3: Протестировать workflows
|
||||
|
||||
- Запустить тестовые запросы через Postman/curl
|
||||
- Проверить, что коды сохраняются в Redis
|
||||
- Проверить, что SMS отправляются
|
||||
|
||||
### Шаг 4: Изменить PHP код
|
||||
|
||||
**Вариант A: Прокси через PHP (проще)**
|
||||
```php
|
||||
// sms-verify.php просто перенаправляет на n8n
|
||||
$n8n_url = 'https://n8n.clientright.pro/webhook/sms-send';
|
||||
// ... curl запрос к n8n
|
||||
```
|
||||
|
||||
**Вариант B: Прямой вызов n8n из JS (лучше)**
|
||||
```javascript
|
||||
// js/common.js
|
||||
$.ajax({
|
||||
url: 'https://n8n.clientright.pro/webhook/sms-send',
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Шаг 5: Удалить старую логику из PHP
|
||||
|
||||
- Удалить генерацию кода
|
||||
- Удалить работу с Redis (оставить только fallback на файлы, если нужно)
|
||||
- Оставить только проксирование запросов
|
||||
|
||||
---
|
||||
|
||||
## Преимущества
|
||||
|
||||
✅ **Визуализация** - видно весь процесс в n8n
|
||||
✅ **Логирование** - автоматические логи каждого шага
|
||||
✅ **Мониторинг** - видно ошибки и задержки
|
||||
✅ **Гибкость** - легко добавить новые шаги
|
||||
✅ **Тестирование** - можно тестировать каждый шаг отдельно
|
||||
✅ **Масштабируемость** - легко добавить несколько SMS провайдеров
|
||||
|
||||
---
|
||||
|
||||
## Примеры кода для n8n
|
||||
|
||||
### Нормализация номера (Function Node)
|
||||
|
||||
```javascript
|
||||
const phone = $input.item.json.phonenumber || '';
|
||||
const cleaned = phone
|
||||
.replace(/[() -]/g, '')
|
||||
.replace(/^(\+?7|8)/, '');
|
||||
|
||||
return { json: { phone_cleaned: cleaned } };
|
||||
```
|
||||
|
||||
### Генерация кода (Function Node)
|
||||
|
||||
```javascript
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
return { json: { code } };
|
||||
```
|
||||
|
||||
### Генерация токена (Function Node)
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
return { json: { token } };
|
||||
```
|
||||
201
docs/n8n_redis_example.md
Normal file
201
docs/n8n_redis_example.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Пример использования Redis в n8n через Webhook
|
||||
|
||||
## Как это работает
|
||||
|
||||
```
|
||||
Webhook (точка входа) → Redis Node (чтение/запись) → Ответ
|
||||
```
|
||||
|
||||
## Workflow: Проверка SMS кода
|
||||
|
||||
### Структура:
|
||||
|
||||
```
|
||||
1. Webhook Trigger
|
||||
↓
|
||||
2. Function Node (нормализация номера)
|
||||
↓
|
||||
3. Redis Node (GET) - читаем код из Redis
|
||||
↓
|
||||
4. Function Node (сравнение кодов)
|
||||
↓
|
||||
5. Redis Node (DEL) - удаляем код после проверки
|
||||
↓
|
||||
6. Respond to Webhook
|
||||
```
|
||||
|
||||
### Детализация:
|
||||
|
||||
#### Шаг 1: Webhook Trigger
|
||||
- **URL:** `/webhook/sms-verify`
|
||||
- **Method:** POST
|
||||
- **Body:**
|
||||
```json
|
||||
{
|
||||
"phonenumber": "+79262306381",
|
||||
"code": "106574"
|
||||
}
|
||||
```
|
||||
|
||||
#### Шаг 2: Function Node - Нормализация
|
||||
```javascript
|
||||
// Код для Function Node
|
||||
const phone = $input.item.json.phonenumber || '';
|
||||
const cleaned = phone
|
||||
.replace(/[() -]/g, '')
|
||||
.replace(/^(\+?7|8)/, '');
|
||||
|
||||
return {
|
||||
json: {
|
||||
phone_cleaned: cleaned,
|
||||
code: $input.item.json.code
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Шаг 3: Redis Node - Чтение кода
|
||||
- **Operation:** Get
|
||||
- **Key:** `sms:code:{{ $json.phone_cleaned }}`
|
||||
- **Результат:**
|
||||
```json
|
||||
{
|
||||
"phone_cleaned": "9262306381",
|
||||
"code": "106574",
|
||||
"stored_code": "106574" // из Redis
|
||||
}
|
||||
```
|
||||
|
||||
#### Шаг 4: Function Node - Сравнение
|
||||
```javascript
|
||||
// Код для Function Node
|
||||
const inputCode = $input.item.json.code;
|
||||
const storedCode = $input.item.json.stored_code;
|
||||
|
||||
if (!storedCode) {
|
||||
return {
|
||||
json: {
|
||||
success: false,
|
||||
message: "Код не найден или истек"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (inputCode !== storedCode) {
|
||||
return {
|
||||
json: {
|
||||
success: false,
|
||||
message: "Неверный код"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Код верный - продолжаем
|
||||
return {
|
||||
json: {
|
||||
success: true,
|
||||
phone_cleaned: $input.item.json.phone_cleaned
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Шаг 5: Redis Node - Удаление кода
|
||||
- **Operation:** Delete
|
||||
- **Key:** `sms:code:{{ $json.phone_cleaned }}`
|
||||
|
||||
#### Шаг 6: Redis Node - Создание токена
|
||||
- **Operation:** Set
|
||||
- **Key:** `sms:verified:{{ $json.phone_cleaned }}`
|
||||
- **Value:** `{{ $json.token }}` (генерируется в Function Node)
|
||||
- **TTL:** 3600 секунд
|
||||
|
||||
#### Шаг 7: Respond to Webhook
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Код подтвержден",
|
||||
"token": "abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow: Отправка SMS кода
|
||||
|
||||
### Структура:
|
||||
|
||||
```
|
||||
1. Webhook Trigger
|
||||
↓
|
||||
2. Function Node (нормализация)
|
||||
↓
|
||||
3. Redis Node (GET) - проверка rate limit
|
||||
↓
|
||||
4. Function Node (генерация кода)
|
||||
↓
|
||||
5. Redis Node (SETEX) - сохранение кода
|
||||
↓
|
||||
6. HTTP Request - отправка SMS
|
||||
↓
|
||||
7. Respond to Webhook
|
||||
```
|
||||
|
||||
### Детализация:
|
||||
|
||||
#### Шаг 3: Redis Node - Проверка rate limit
|
||||
- **Operation:** Get
|
||||
- **Key:** `sms:ratelimit:send:{{ $json.phone_cleaned }}`
|
||||
- **Если значение >= 5** → ошибка через IF Node
|
||||
|
||||
#### Шаг 5: Redis Node - Сохранение кода
|
||||
- **Operation:** Set with Expiration
|
||||
- **Key:** `sms:code:{{ $json.phone_cleaned }}`
|
||||
- **Value:** `{{ $json.code }}`
|
||||
- **TTL:** 600 секунд (10 минут)
|
||||
|
||||
#### Шаг 6: HTTP Request - Отправка SMS
|
||||
- **Method:** POST
|
||||
- **URL:** (SMS API провайдера)
|
||||
- **Body:**
|
||||
```json
|
||||
{
|
||||
"phone": "{{ $json.phone_cleaned }}",
|
||||
"text": "Код подтверждения: {{ $json.code }}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Настройка Redis в n8n
|
||||
|
||||
### Credentials:
|
||||
|
||||
1. Зайти в n8n → Credentials → New
|
||||
2. Выбрать "Redis"
|
||||
3. Заполнить:
|
||||
- **Host:** `crm.clientright.ru`
|
||||
- **Port:** `6379`
|
||||
- **Password:** (из .env или пусто)
|
||||
- **Database:** `0` (по умолчанию)
|
||||
|
||||
### Использование в Node:
|
||||
|
||||
1. Добавить **Redis Node** в workflow
|
||||
2. Выбрать созданные credentials
|
||||
3. Выбрать операцию (Get, Set, Set with Expiration, Delete, etc.)
|
||||
4. Указать ключ (можно использовать переменные: `sms:code:{{ $json.phone_cleaned }}`)
|
||||
|
||||
---
|
||||
|
||||
## Преимущества
|
||||
|
||||
✅ **Всё в одном месте** - webhook принимает запрос, Redis Node читает/пишет
|
||||
✅ **Визуализация** - видно весь процесс на графике
|
||||
✅ **Логирование** - n8n автоматически логирует все операции
|
||||
✅ **Обработка ошибок** - можно добавить IF Nodes для проверок
|
||||
✅ **Тестирование** - можно тестировать каждый шаг отдельно
|
||||
|
||||
---
|
||||
|
||||
## Пример полного workflow (JSON для импорта в n8n)
|
||||
|
||||
Можно создать workflow вручную или импортировать готовый JSON (если нужно, могу создать).
|
||||
150
docs/n8n_sms_workflow.md
Normal file
150
docs/n8n_sms_workflow.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# N8N Workflow для SMS верификации
|
||||
|
||||
## Текущая архитектура
|
||||
|
||||
```
|
||||
Frontend (JS) → PHP (sms-verify.php) → Redis + n8n (только отправка SMS)
|
||||
```
|
||||
|
||||
## Предлагаемая архитектура
|
||||
|
||||
```
|
||||
Frontend (JS) → n8n Webhook → Redis + SMS отправка (всё в n8n)
|
||||
```
|
||||
|
||||
## Workflow 1: Отправка SMS кода
|
||||
|
||||
**Webhook URL:** `https://n8n.clientright.pro/webhook/sms-send`
|
||||
|
||||
### Шаги:
|
||||
|
||||
1. **Webhook Trigger** (POST)
|
||||
- Принимает: `{ "phonenumber": "+79262306381" }`
|
||||
|
||||
2. **Нормализация номера телефона**
|
||||
- Убрать пробелы, скобки, дефисы
|
||||
- Убрать +7 или 8 в начале
|
||||
- Результат: `9262306381`
|
||||
|
||||
3. **Проверка Rate Limit (Redis)**
|
||||
- Ключ: `sms:ratelimit:send:9262306381`
|
||||
- Если значение >= 5 → ошибка "Превышен лимит"
|
||||
- Иначе → увеличить счетчик (TTL: 1 час)
|
||||
|
||||
4. **Генерация кода**
|
||||
- 6-значный случайный код: `106574`
|
||||
|
||||
5. **Сохранение кода в Redis**
|
||||
- Ключ: `sms:code:9262306381`
|
||||
- Значение: `106574`
|
||||
- TTL: 600 секунд (10 минут)
|
||||
|
||||
6. **Отправка SMS (HTTP Request)**
|
||||
- URL: API SMS провайдера (SigmaSMS или другой)
|
||||
- Метод: POST
|
||||
- Body: `{ "phone": "9262306381", "text": "Код подтверждения: 106574" }`
|
||||
|
||||
7. **Обработка ошибок**
|
||||
- Если SMS не отправилось → удалить код из Redis
|
||||
- Вернуть ошибку
|
||||
|
||||
8. **Ответ**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Код отправлен на ваш номер телефона"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow 2: Проверка SMS кода
|
||||
|
||||
**Webhook URL:** `https://n8n.clientright.pro/webhook/sms-verify`
|
||||
|
||||
### Шаги:
|
||||
|
||||
1. **Webhook Trigger** (POST)
|
||||
- Принимает: `{ "phonenumber": "+79262306381", "code": "106574" }`
|
||||
|
||||
2. **Нормализация номера телефона**
|
||||
- Аналогично workflow 1
|
||||
|
||||
3. **Проверка Rate Limit для попыток (Redis)**
|
||||
- Ключ: `sms:ratelimit:attempts:9262306381`
|
||||
- Если значение >= 10 → ошибка "Превышено количество попыток"
|
||||
- Иначе → увеличить счетчик (TTL: 15 минут)
|
||||
|
||||
4. **Чтение кода из Redis**
|
||||
- Ключ: `sms:code:9262306381`
|
||||
- Если не найден → ошибка "Код не найден или истек"
|
||||
|
||||
5. **Сравнение кодов**
|
||||
- Если `введенный код !== сохраненный код` → ошибка "Неверный код"
|
||||
|
||||
6. **Успешная верификация**
|
||||
- Удалить код из Redis: `sms:code:9262306381`
|
||||
- Удалить счетчик попыток: `sms:ratelimit:attempts:9262306381`
|
||||
- Создать токен верификации: `sms:verified:9262306381` = `random_token` (TTL: 1 час)
|
||||
|
||||
7. **Ответ**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Код подтвержден",
|
||||
"token": "abc123def456..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow 3: Проверка статуса верификации
|
||||
|
||||
**Webhook URL:** `https://n8n.clientright.pro/webhook/sms-check-verified`
|
||||
|
||||
### Шаги:
|
||||
|
||||
1. **Webhook Trigger** (POST)
|
||||
- Принимает: `{ "phonenumber": "+79262306381", "token": "abc123..." }`
|
||||
|
||||
2. **Нормализация номера**
|
||||
|
||||
3. **Проверка токена в Redis**
|
||||
- Ключ: `sms:verified:9262306381`
|
||||
- Если токен совпадает → `verified: true`
|
||||
- Иначе → `verified: false`
|
||||
|
||||
4. **Ответ**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"verified": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Преимущества переноса в n8n
|
||||
|
||||
1. ✅ **Визуализация** - видно весь процесс на графике
|
||||
2. ✅ **Логирование** - все шаги логируются автоматически
|
||||
3. ✅ **Мониторинг** - видно ошибки и задержки
|
||||
4. ✅ **Гибкость** - легко добавить новые шаги (например, уведомления в Telegram)
|
||||
5. ✅ **Тестирование** - можно тестировать каждый шаг отдельно
|
||||
6. ✅ **Масштабируемость** - легко добавить несколько SMS провайдеров с fallback
|
||||
|
||||
---
|
||||
|
||||
## Изменения в PHP коде
|
||||
|
||||
После переноса в n8n, `sms-verify.php` станет простым прокси:
|
||||
|
||||
```php
|
||||
// sms-verify.php?action=send
|
||||
// Просто перенаправляет на n8n webhook
|
||||
|
||||
// sms-verify.php?action=verify
|
||||
// Просто перенаправляет на n8n webhook
|
||||
```
|
||||
|
||||
Или можно вообще убрать PHP и вызывать n8n напрямую из JS.
|
||||
230
docs/n8n_webhook_response.md
Normal file
230
docs/n8n_webhook_response.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Формат ответа N8N Webhook для проверки SMS кода
|
||||
|
||||
## URL Webhook
|
||||
`https://n8n.clientright.pro/webhook/erv_sms_verify`
|
||||
|
||||
## Входящие данные (POST)
|
||||
|
||||
```json
|
||||
{
|
||||
"phonenumber": "+79262306381",
|
||||
"code": "106574"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Формат ответа
|
||||
|
||||
### ✅ Успешная проверка (HTTP 200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Код подтвержден"
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `success` (boolean, обязательное) - `true` при успехе
|
||||
- `message` (string, обязательное) - сообщение для пользователя
|
||||
|
||||
---
|
||||
|
||||
### ❌ Ошибки (HTTP 400)
|
||||
|
||||
#### 1. Неверный код
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "invalid_code",
|
||||
"message": "Неверный код"
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `success` (boolean) - `false`
|
||||
- `error` (string) - код ошибки: `"invalid_code"`
|
||||
- `message` (string) - сообщение для пользователя
|
||||
|
||||
---
|
||||
|
||||
#### 2. Код не найден или истек
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "code_not_found",
|
||||
"message": "Код не найден или истек. Запросите новый код."
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `success` (boolean) - `false`
|
||||
- `error` (string) - код ошибки: `"code_not_found"`
|
||||
- `message` (string) - сообщение для пользователя
|
||||
|
||||
---
|
||||
|
||||
#### 3. Превышен лимит попыток
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "rate_limit_exceeded",
|
||||
"message": "Превышено количество попыток. Попробуйте позже."
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `success` (boolean) - `false`
|
||||
- `error` (string) - код ошибки: `"rate_limit_exceeded"`
|
||||
- `message` (string) - сообщение для пользователя
|
||||
|
||||
---
|
||||
|
||||
#### 4. Номер телефона или код не указаны
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "missing_data",
|
||||
"message": "Номер телефона или код не указаны"
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `success` (boolean) - `false`
|
||||
- `error` (string) - код ошибки: `"missing_data"`
|
||||
- `message` (string) - сообщение для пользователя
|
||||
|
||||
---
|
||||
|
||||
#### 5. Внутренняя ошибка сервера (HTTP 500)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "internal_error",
|
||||
"message": "Сервис временно недоступен. Попробуйте позже."
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `success` (boolean) - `false`
|
||||
- `error` (string) - код ошибки: `"internal_error"`
|
||||
- `message` (string) - сообщение для пользователя
|
||||
|
||||
---
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
| Код ошибки | HTTP Status | Описание |
|
||||
|------------|-------------|----------|
|
||||
| `invalid_code` | 400 | Введенный код не совпадает с сохраненным |
|
||||
| `code_not_found` | 400 | Код не найден в Redis или истек (TTL) |
|
||||
| `rate_limit_exceeded` | 400 | Превышен лимит попыток проверки (10 за 15 минут) |
|
||||
| `missing_data` | 400 | Не указан номер телефона или код |
|
||||
| `internal_error` | 500 | Внутренняя ошибка (Redis недоступен и т.д.) |
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования в n8n
|
||||
|
||||
### Успешная проверка
|
||||
|
||||
**Workflow шаги:**
|
||||
1. Webhook Trigger → получает `phonenumber` и `code`
|
||||
2. Function Node → нормализация номера
|
||||
3. Redis Node (GET) → чтение кода: `sms:code:9262306381`
|
||||
4. Function Node → сравнение кодов
|
||||
5. Redis Node (DEL) → удаление кода и счетчика попыток
|
||||
6. **Respond to Webhook** → возвращает успешный ответ
|
||||
|
||||
**Код для Respond to Webhook:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Код подтвержден"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Ошибка: Неверный код
|
||||
|
||||
**Workflow шаги:**
|
||||
1. ... (аналогично успешной проверке)
|
||||
2. Function Node → сравнение кодов
|
||||
3. IF Node → если коды не совпадают
|
||||
4. **Respond to Webhook** → возвращает ошибку
|
||||
|
||||
**Код для Respond to Webhook:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "invalid_code",
|
||||
"message": "Неверный код"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Ошибка: Код не найден
|
||||
|
||||
**Workflow шаги:**
|
||||
1. ... (аналогично)
|
||||
2. Redis Node (GET) → чтение кода
|
||||
3. IF Node → если код = null
|
||||
4. **Respond to Webhook** → возвращает ошибку
|
||||
|
||||
**Код для Respond to Webhook:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "code_not_found",
|
||||
"message": "Код не найден или истек. Запросите новый код."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Совместимость с текущим кодом
|
||||
|
||||
Текущий JS код (`common.js`) ожидает:
|
||||
|
||||
```javascript
|
||||
success: function(data) {
|
||||
if (data.success) {
|
||||
// Успех - сохраняем токен
|
||||
sms_verify_token = data.token;
|
||||
// ... показываем форму
|
||||
} else {
|
||||
// Ошибка - показываем сообщение
|
||||
$('.modal .form-item__warning').text(data.message || "Неверный код");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Предложенный формат полностью совместим!** ✅
|
||||
|
||||
---
|
||||
|
||||
## Дополнительные рекомендации
|
||||
|
||||
1. **HTTP Status Codes:**
|
||||
- Успех: `200 OK`
|
||||
- Ошибки клиента: `400 Bad Request`
|
||||
- Ошибки сервера: `500 Internal Server Error`
|
||||
|
||||
2. **Content-Type:**
|
||||
- Всегда: `application/json; charset=utf-8`
|
||||
|
||||
3. **Логирование:**
|
||||
- Логировать все попытки проверки (успешные и неуспешные)
|
||||
- Логировать ошибки с деталями
|
||||
|
||||
4. **Безопасность:**
|
||||
- Не возвращать детали внутренних ошибок в production
|
||||
- Не возвращать реальные коды в ответах
|
||||
@@ -687,8 +687,8 @@ $(function() {
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data.success) {
|
||||
// Сохраняем токен верификации
|
||||
sms_verify_token = data.token;
|
||||
// Код подтвержден (токен больше не используется)
|
||||
// sms_verify_token = data.token; // Убрано - токен больше не нужен
|
||||
|
||||
$('.sms-success').removeClass('d-none');
|
||||
|
||||
@@ -729,7 +729,7 @@ $(function() {
|
||||
var $phoneField = $('.js-phone-mask');
|
||||
var confirmedPhone = $phoneField.val();
|
||||
$phoneField.attr('data-confirmed-phone', confirmedPhone)
|
||||
.attr('data-verify-token', sms_verify_token)
|
||||
// .attr('data-verify-token', sms_verify_token) // Убрано - токен больше не нужен
|
||||
.prop('readonly', true)
|
||||
.attr('autocomplete', 'off');
|
||||
|
||||
|
||||
158
sms-verify.php
158
sms-verify.php
@@ -353,11 +353,10 @@ try {
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} elseif ($action === 'verify') {
|
||||
// Проверка кода
|
||||
// Проверка кода через n8n webhook
|
||||
log_message("=== НАЧАЛО ПРОВЕРКИ КОДА ===");
|
||||
log_message("REQUEST_METHOD: " . ($_SERVER['REQUEST_METHOD'] ?? 'не установлен'));
|
||||
log_message("POST данные: " . json_encode($_POST, JSON_UNESCAPED_UNICODE));
|
||||
log_message("GET данные: " . json_encode($_GET, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$phone = $_POST['phonenumber'] ?? '';
|
||||
$code = $_POST['code'] ?? '';
|
||||
@@ -370,107 +369,88 @@ try {
|
||||
throw new Exception("Номер телефона или код не указаны");
|
||||
}
|
||||
|
||||
$phone_cleaned = clear_phone($phone);
|
||||
log_message("Номер после очистки: '$phone_cleaned'");
|
||||
// Получаем URL webhook из .env
|
||||
$webhook_url = env('N8N_SMS_VERIFY_WEBHOOK', '');
|
||||
|
||||
// Проверка rate limiting - пытаемся переподключиться при необходимости
|
||||
log_message("Попытка подключения к Redis для проверки кода...");
|
||||
$redis = getRedis(true); // Принудительно пытаемся переподключиться
|
||||
|
||||
$stored_code = false;
|
||||
|
||||
$stored_code = false;
|
||||
|
||||
// Пытаемся получить код из Redis
|
||||
if ($redis) {
|
||||
log_message("Redis подключен успешно для проверки кода");
|
||||
try {
|
||||
// Проверяем количество попыток
|
||||
$key_attempts = "sms:ratelimit:attempts:$phone_cleaned";
|
||||
$attempts = $redis->get($key_attempts) ?: 0;
|
||||
|
||||
if ($attempts >= 10) {
|
||||
log_message("Превышено количество попыток для номера $phone_cleaned");
|
||||
throw new Exception("Превышено количество попыток. Попробуйте позже.");
|
||||
if (empty($webhook_url)) {
|
||||
log_message("Ошибка: не указан N8N_SMS_VERIFY_WEBHOOK в .env");
|
||||
throw new Exception("Сервис временно недоступен. Попробуйте позже.");
|
||||
}
|
||||
|
||||
// Увеличиваем счетчик попыток
|
||||
$redis->setex($key_attempts, 900, $attempts + 1); // 15 минут
|
||||
log_message("Отправка запроса на n8n webhook: $webhook_url");
|
||||
|
||||
// Проверяем код
|
||||
$key = "sms:code:$phone_cleaned";
|
||||
log_message("Проверка кода для номера $phone_cleaned, ключ Redis: $key, введенный код: $code");
|
||||
// Отправляем запрос на n8n webhook
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $webhook_url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'phonenumber' => $phone,
|
||||
'code' => $code
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json; charset=utf-8'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
|
||||
$stored_code = $redis->get($key);
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_error = curl_error($ch);
|
||||
$curl_errno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($stored_code !== null) {
|
||||
log_message("Код найден в Redis: $stored_code");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Пробрасываем исключение, если это ошибка rate limiting
|
||||
if (strpos($e->getMessage(), 'Превышено количество попыток') !== false) {
|
||||
throw $e;
|
||||
}
|
||||
log_message("Ошибка при работе с Redis: " . $e->getMessage());
|
||||
}
|
||||
if ($curl_error) {
|
||||
log_message("Ошибка CURL при проверке кода через n8n: $curl_error (код: $curl_errno)");
|
||||
throw new Exception("Ошибка соединения с сервисом. Попробуйте позже.");
|
||||
}
|
||||
|
||||
// Если код не найден в Redis, пытаемся получить из файла (fallback)
|
||||
if ($stored_code === null) {
|
||||
log_message("Код не найден в Redis, проверяем файловое хранилище...");
|
||||
$stored_code = getCodeFromFile($phone_cleaned);
|
||||
if ($stored_code !== false) {
|
||||
log_message("Код найден в файловом хранилище для номера $phone_cleaned");
|
||||
}
|
||||
// Логируем ответ от n8n
|
||||
log_message("Ответ от n8n: HTTP $http_code, ответ: " . substr($response, 0, 200));
|
||||
|
||||
// Парсим ответ
|
||||
$response_data = json_decode($response, true);
|
||||
|
||||
if ($response_data === null) {
|
||||
log_message("Ошибка: не удалось распарсить ответ от n8n: " . substr($response, 0, 200));
|
||||
throw new Exception("Ошибка обработки ответа. Попробуйте позже.");
|
||||
}
|
||||
|
||||
// Если код все еще не найден, выдаем ошибку
|
||||
if ($stored_code === null || $stored_code === false) {
|
||||
log_message("КРИТИЧЕСКАЯ ОШИБКА: Код не найден ни в Redis, ни в файловом хранилище для номера $phone_cleaned");
|
||||
throw new Exception("Код не найден или истек. Запросите новый код.");
|
||||
}
|
||||
|
||||
log_message("Проверка кода: сохраненный=$stored_code, введенный=$code");
|
||||
|
||||
if ($stored_code !== $code) {
|
||||
log_message("Неверный код для номера $phone_cleaned. Введен: $code, ожидался: $stored_code");
|
||||
throw new Exception("Неверный код");
|
||||
}
|
||||
|
||||
// Код верный - удаляем его из Redis и файла, создаем сессию подтверждения
|
||||
if ($redis) {
|
||||
try {
|
||||
$key = "sms:code:$phone_cleaned";
|
||||
$redis->del($key);
|
||||
$key_attempts = "sms:ratelimit:attempts:$phone_cleaned";
|
||||
$redis->del($key_attempts); // Сбрасываем счетчик попыток
|
||||
} catch (Exception $e) {
|
||||
log_message("Ошибка при удалении кода из Redis: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
deleteCodeFromFile($phone_cleaned); // Удаляем из файла тоже
|
||||
|
||||
// Создаем токен подтверждения (действует 1 час) - только если Redis доступен
|
||||
$verify_token = bin2hex(random_bytes(32));
|
||||
if ($redis) {
|
||||
try {
|
||||
$verify_key = "sms:verified:$phone_cleaned";
|
||||
$redis->setex($verify_key, 3600, $verify_token);
|
||||
log_message("Токен верификации сохранен в Redis для номера $phone_cleaned");
|
||||
} catch (Exception $e) {
|
||||
log_message("Ошибка при создании токена верификации: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
log_message("Токен верификации сгенерирован, но не сохранен (Redis недоступен)");
|
||||
}
|
||||
|
||||
log_message("Код подтвержден для номера: $phone_cleaned");
|
||||
// Проверяем успешность
|
||||
if ($http_code >= 200 && $http_code < 300) {
|
||||
// Успешный ответ от n8n
|
||||
if (isset($response_data['success']) && $response_data['success'] === true) {
|
||||
log_message("Код подтвержден через n8n для номера: " . ($phone ?: 'не указан'));
|
||||
|
||||
// Возвращаем упрощенный ответ (без токена)
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Код подтвержден',
|
||||
'token' => $verify_token // Токен для последующей проверки
|
||||
'message' => $response_data['message'] ?? 'Код подтвержден'
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
// Ошибка от n8n
|
||||
$error_message = $response_data['message'] ?? 'Неверный код';
|
||||
log_message("Ошибка проверки кода через n8n: $error_message");
|
||||
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $error_message
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
} else {
|
||||
// HTTP ошибка
|
||||
$error_message = $response_data['message'] ?? "Ошибка сервиса (HTTP $http_code)";
|
||||
log_message("HTTP ошибка от n8n: $http_code, сообщение: $error_message");
|
||||
|
||||
http_response_code($http_code >= 500 ? 500 : 400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $error_message
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
} elseif ($action === 'check_verified') {
|
||||
// Проверка статуса верификации (для проверки перед отправкой формы)
|
||||
|
||||
Reference in New Issue
Block a user