docs: добавить планы улучшения архитектуры SFERA

This commit is contained in:
Veronika Smirnova
2025-09-18 21:28:07 +03:00
parent 733ccadeb7
commit 3efc387308
18 changed files with 3130 additions and 74 deletions

View File

@ -0,0 +1,488 @@
# 🔐 ПЛАН РЕАЛИЗАЦИИ БЕЗОПАСНОЙ СИСТЕМЫ API КЛЮЧЕЙ
> **Дата:** 2025-09-18
> **Проект:** SFERA
> **Статус:** ГОТОВ К РЕАЛИЗАЦИИ
## 📊 РЕЗУЛЬТАТЫ ДИАГНОСТИКИ
### ✅ ТЕКУЩЕЕ СОСТОЯНИЕ СИСТЕМЫ:
**Данные в БД:**
- 1 активный API ключ Wildberries (длина 398 символов)
- Организация "Rennel" типа SELLER
- Ключ создан: 2025-09-17T15:19:35.423Z
- **ПРОБЛЕМА:** Хранится в открытом виде в поле `apiKey`
**UI Состояние:**
- ✅ Звездочки отображаются корректно: `••••••••••••••••••••`
- ✅ Логика переключения работает (редактирование очищает поле)
- ✅ Индикатор статуса показывает "API ключ настроен"
-**КРИТИЧНО:** handleSave НЕ СОХРАНЯЕТ API ключи
**Безопасность:**
- ❌ Ключи хранятся в открытом виде в БД
- ❌ ENCRYPTION_KEY отсутствует в .env
- ⚠️ Нужно проверить копирование звездочек
### 🎯 ЦЕЛИ РЕАЛИЗАЦИИ:
1. **Безопасность:** Зашифровать существующий API ключ
2. **Функциональность:** Исправить сохранение в handleSave
3. **UX:** Защитить от копирования звездочек
4. **Производительность:** Кэш расшифровки на 2 часа
---
## 🛡️ ПОЭТАПНЫЙ ПЛАН РЕАЛИЗАЦИИ
### 📋 ЭТАП 1: ТЕСТИРОВАНИЕ КОПИРОВАНИЯ (10 мин)
**Цель:** Убедиться что звездочки не раскрывают реальный ключ
**Задачи:**
- [ ] Запустить dev сервер
- [ ] Открыть /seller/settings в браузере
- [ ] Протестировать копирование поля с звездочками
- [ ] При необходимости добавить атрибуты блокировки
**Ожидаемый результат:** Звездочки нельзя скопировать как реальный ключ
**Критерии успеха:**
- ✅ При копировании получаются звездочки, а не настоящий ключ
- ✅ Невозможно выделить реальный ключ из поля
---
### 🔧 ЭТАП 2: ИСПРАВЛЕНИЕ СОХРАНЕНИЯ (30 мин)
**Цель:** handleSave начинает сохранять API ключи в БД
**Подготовка:**
```bash
# Backup текущего состояния
cp src/components/dashboard/user-settings.tsx src/components/dashboard/user-settings.tsx.backup
```
**Задачи:**
- [ ] Найти где обрезана функция handleSave (после строки 698)
- [ ] Добавить импорт ADD_MARKETPLACE_API_KEY в user-settings.tsx
- [ ] Добавить логику сохранения API ключей после сохранения профиля
- [ ] Протестировать сохранение через UI
**Код изменений:**
```typescript
// В импортах добавить:
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN, ADD_MARKETPLACE_API_KEY } from '@/graphql/mutations'
// В handleSave добавить после сохранения профиля:
// Сохраняем API ключи маркетплейсов
if (formData.wildberriesApiKey && formData.wildberriesApiKey !== '••••••••••••••••••••') {
await apolloClient.mutate({
mutation: ADD_MARKETPLACE_API_KEY,
variables: {
input: {
marketplace: 'WILDBERRIES',
apiKey: formData.wildberriesApiKey,
validateOnly: false
}
}
})
}
if (formData.ozonApiKey && formData.ozonApiKey !== '••••••••••••••••••••') {
await apolloClient.mutate({
mutation: ADD_MARKETPLACE_API_KEY,
variables: {
input: {
marketplace: 'OZON',
apiKey: formData.ozonApiKey,
clientId: formData.ozonClientId,
validateOnly: false
}
}
})
}
// Обновляем кэш с новыми API ключами
await apolloClient.refetchQueries({
include: [GET_ME]
})
```
**Тестирование:**
- [ ] Ввести новый WB ключ → должен сохраниться в БД
- [ ] Проверить что ключ виден в GraphQL ответе
- [ ] Проверить работу статистики с новым ключом
**Rollback план:** Восстановить из backup файла
---
### 🔐 ЭТАП 3: СОЗДАНИЕ КРИПТОСЕРВИСА (45 мин)
**Цель:** Безопасное шифрование с кэшем на 2 часа
**Подготовка:**
```bash
# Генерация мастер-ключа
openssl rand -hex 32
# Добавление в .env
echo "ENCRYPTION_KEY=сгенерированный_ключ" >> .env
```
**Задачи:**
- [ ] Создать `/src/services/crypto.service.ts`
- [ ] Реализовать AES-256-GCM шифрование
- [ ] Добавить кэш с TTL 2 часа
- [ ] Написать юнит-тесты
**Реализация CryptoService:**
```typescript
// src/services/crypto.service.ts
import crypto from 'crypto'
export class CryptoService {
private algorithm = 'aes-256-gcm'
private key: Buffer
// Кэш на 2 часа
private cache = new Map<string, { value: string; expires: number }>()
constructor() {
const masterKey = process.env.ENCRYPTION_KEY
if (!masterKey || masterKey.length !== 64) {
throw new Error('ENCRYPTION_KEY must be 64 hex characters')
}
this.key = Buffer.from(masterKey, 'hex')
}
encrypt(text: string): { encrypted: string; iv: string; tag: string } {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv)
let encrypted = cipher.update(text, 'utf8', 'hex')
encrypted += cipher.final('hex')
return {
encrypted,
iv: iv.toString('hex'),
tag: cipher.getAuthTag().toString('hex')
}
}
decrypt(encrypted: string, iv: string, tag: string): string {
// Проверяем кэш (2 часа TTL)
const cacheKey = `${encrypted}-${iv}`
const cached = this.cache.get(cacheKey)
if (cached && cached.expires > Date.now()) {
return cached.value
}
// Расшифровываем
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(iv, 'hex')
)
decipher.setAuthTag(Buffer.from(tag, 'hex'))
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
// Сохраняем в кэш на 2 часа
this.cache.set(cacheKey, {
value: decrypted,
expires: Date.now() + 2 * 60 * 60 * 1000 // 2 часа
})
// Очищаем старые записи
this.cleanCache()
return decrypted
}
private cleanCache() {
const now = Date.now()
for (const [key, item] of this.cache) {
if (item.expires < now) {
this.cache.delete(key)
}
}
}
}
```
**Тестирование:**
- [ ] Шифрование/расшифровка работает корректно
- [ ] Кэш сохраняет данные на 2 часа
- [ ] Автоочистка удаляет старые записи
---
### 🗄️ ЭТАП 4: МИГРАЦИЯ БД (30 мин)
**Цель:** Добавить поля для зашифрованных ключей
**Задачи:**
- [ ] Создать Prisma миграцию с новыми полями
- [ ] Применить миграцию к БД
- [ ] Проверить совместимость
**Новая схема:**
```prisma
model ApiKey {
id String @id @default(cuid())
marketplace MarketplaceType
// Новые поля для шифрования
encryptedKey String? // Зашифрованный ключ
encryptionIv String? // Вектор инициализации
encryptionTag String? // Тег аутентификации
lastUsedAt DateTime? // Аудит использования
// Старое поле (временно для совместимости)
apiKey String // УДАЛИТЬ после миграции
clientId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
validationData Json?
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
@@unique([organizationId, marketplace])
@@map("api_keys")
}
```
**Команды миграции:**
```bash
npx prisma migrate dev --name add_api_keys_encryption
npx prisma generate
```
**Тестирование:**
- [ ] Миграция применяется без ошибок
- [ ] Существующий ключ остается рабочим
- [ ] Новые поля готовы к использованию
**Rollback:** `npx prisma migrate reset` (данные не критичны)
---
### 🔄 ЭТАП 5: ОБНОВЛЕНИЕ РЕЗОЛВЕРОВ (45 мин)
**Цель:** Использовать шифрование в GraphQL
**Задачи:**
- [ ] Обновить addMarketplaceApiKey для шифрования новых ключей
- [ ] Обновить getWildberriesStatistics для расшифровки
- [ ] Добавить fallback на старые ключи
- [ ] Протестировать API
**Изменения в wildberries.ts:**
```typescript
import { CryptoService } from '../../../services/crypto.service'
const cryptoService = new CryptoService()
// В addMarketplaceApiKey:
if (existing) {
// Шифруем новый ключ
const encrypted = cryptoService.encrypt(apiKey)
await prisma.apiKey.update({
where: { id: existing.id },
data: {
encryptedKey: encrypted.encrypted,
encryptionIv: encrypted.iv,
encryptionTag: encrypted.tag,
apiKey: null, // Очищаем старое поле
isActive: true
}
})
}
// В getWildberriesStatistics:
let decryptedKey: string
if (apiKey.encryptedKey && apiKey.encryptionIv && apiKey.encryptionTag) {
// Новый зашифрованный ключ
decryptedKey = cryptoService.decrypt(
apiKey.encryptedKey,
apiKey.encryptionIv,
apiKey.encryptionTag
)
} else if (apiKey.apiKey) {
// Fallback на старый незашифрованный ключ
decryptedKey = apiKey.apiKey
} else {
return { success: false, message: 'API ключ не найден' }
}
```
**Тестирование:**
- [ ] Новые ключи шифруются при сохранении
- [ ] Старый ключ работает через fallback
- [ ] Статистика загружается корректно
- [ ] Кэш работает (повторные запросы быстрее)
---
### 📦 ЭТАП 6: МИГРАЦИЯ СУЩЕСТВУЮЩЕГО КЛЮЧА (15 мин)
**Цель:** Перешифровать единственный ключ в БД
**Задачи:**
- [ ] Создать скрипт миграции
- [ ] Выполнить перешифровку
- [ ] Убрать fallback логику
- [ ] Финальное тестирование
**Скрипт миграции:**
```javascript
// scripts/migrate-existing-api-keys.js
import { PrismaClient } from '@prisma/client'
import { CryptoService } from '../src/services/crypto.service.js'
const prisma = new PrismaClient()
const cryptoService = new CryptoService()
async function migrateApiKeys() {
const keys = await prisma.apiKey.findMany({
where: {
apiKey: { not: null },
encryptedKey: null
}
})
console.log(`🔄 Найдено ключей для миграции: ${keys.length}`)
for (const key of keys) {
if (!key.apiKey) continue
const encrypted = cryptoService.encrypt(key.apiKey)
await prisma.apiKey.update({
where: { id: key.id },
data: {
encryptedKey: encrypted.encrypted,
encryptionIv: encrypted.iv,
encryptionTag: encrypted.tag,
apiKey: null // Очищаем старое поле
}
})
console.log(`✅ Мигрирован ключ ${key.id} для ${key.marketplace}`)
}
console.log('🎉 Миграция завершена')
}
migrateApiKeys().catch(console.error)
```
**Выполнение:**
```bash
node scripts/migrate-existing-api-keys.js
```
**Тестирование:**
- [ ] Существующий ключ перешифрован
- [ ] Статистика продолжает работать
- [ ] В БД нет незашифрованных ключей
- [ ] Fallback больше не используется
---
## 🧪 ПЛАН ТЕСТИРОВАНИЯ
### После каждого этапа проверять:
**1. Основная функциональность:**
- [ ] Авторизация в кабинет селлера работает
- [ ] Настройки → API ключи открываются
- [ ] Статистика Wildberries загружается
**2. Сохранение ключей:**
- [ ] Ввод нового WB ключа сохраняется
- [ ] Настройки сохраняются без ошибок
- [ ] Ключ появляется в БД
**3. Безопасность:**
- [ ] Звездочки показываются вместо ключей
- [ ] Копирование не раскрывает реальные ключи
- [ ] GraphQL не возвращает расшифрованные ключи
- [ ] Ошибки не логируют реальные ключи
**4. Производительность:**
- [ ] Первый запрос статистики (расшифровка)
- [ ] Повторный запрос (из кэша) быстрее
- [ ] Память не растет при множественных запросах
---
## ⚠️ КРИТИЧЕСКИЕ МОМЕНТЫ
### 🚨 ТОЧКИ ОСТАНОВКИ:
- Статистика перестала загружаться
- Ошибки сохранения в настройках
- Потеря существующего API ключа
- Любые 500 ошибки в GraphQL
- Ошибки шифрования/расшифровки
### 🔄 ROLLBACK ПЛАНЫ:
**Этап 2:** Восстановить user-settings.tsx из backup
**Этап 3:** Не применять к продакшену
**Этап 4:** `npx prisma migrate reset`
**Этап 5:** Откатить изменения резолверов
**Этап 6:** Восстановить из backup БД
### 💾 BACKUP ФАЙЛЫ:
-`api_keys_backup_1758196936364.json` - данные БД
-`user-settings.tsx.backup` - будет создан на этапе 2
---
## 🎯 ОЖИДАЕМЫЙ РЕЗУЛЬТАТ
### После завершения всех этапов:
**✅ Безопасность:**
- API ключи зашифрованы в БД алгоритмом AES-256-GCM
- UI показывает только звездочки
- Копирование не раскрывает реальные ключи
- GraphQL не возвращает расшифрованные ключи
**✅ Функциональность:**
- Сохранение API ключей через UI работает
- Статистика Wildberries загружается корректно
- Существующий ключ мигрирован без потерь
**✅ Производительность:**
- Кэш расшифровки на 2 часа
- Автоочистка старых записей кэша
- Быстрые повторные запросы
**✅ Аудит:**
- Поле lastUsedAt отслеживает использование
- Логи не содержат реальных ключей
- Безопасное хранение в production
---
## 📋 ЧЕКЛИСТ ГОТОВНОСТИ
Перед началом убедиться:
- [ ] Проект запускается локально
- [ ] БД доступна и работает
- [ ] Есть доступ к кабинету селлера
- [ ] Backup данных создан
- [ ] ENCRYPTION_KEY сгенерирован
**План готов к исполнению! 🚀**
---
*Документ создан: 2025-09-18*
*Проект: SFERA Marketplace Platform*
*Статус: Готов к реализации*