Files
sfera-new/docs/API_KEYS_SIMPLE_PLAN.md
Veronika Smirnova b6935428ab docs: добавить планы по API ключам и отладке статистики
- API_KEYS_IMPLEMENTATION_PLAN.md - план реализации системы API ключей
- API_KEYS_SECURITY_PLAN.md - план безопасности API ключей
- API_KEYS_SIMPLE_PLAN.md - упрощенный план API ключей
- DEBUG_SELLER_STATISTICS.md - отладка статистики селлеров
- FIX_API_KEYS_SAVING.md - исправление сохранения API ключей

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 21:31:27 +03:00

259 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔐 УПРОЩЕННЫЙ ПЛАН СИСТЕМЫ API КЛЮЧЕЙ
## 📋 ТРЕБОВАНИЯ (УТОЧНЕННЫЕ)
1. **Один ключ на маркетплейс** для организации
2. **Шифрование** всех ключей в БД
3. **Быстрый доступ** без задержек
4. **Безопасный UI** - только звездочки, не копируются
5. **Простота** - минимум сложности
## 🏗️ АРХИТЕКТУРА
### 1. ПРОСТАЯ СХЕМА БД
```prisma
model ApiKey {
id String @id @default(cuid())
marketplace MarketplaceType // WILDBERRIES или OZON
encryptedKey String // Зашифрованный ключ
encryptionIv String // Для расшифровки
encryptionTag String // Для проверки целостности
clientId String? // Только для Ozon
isActive Boolean @default(true)
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, marketplace]) // Один ключ на маркетплейс
@@map("api_keys")
}
```
### 2. ПРОСТОЙ СЕРВИС ШИФРОВАНИЯ
```typescript
// src/services/crypto.service.ts
import crypto from 'crypto'
export class CryptoService {
private algorithm = 'aes-256-gcm'
private key: Buffer
// Простой кэш для производительности
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 {
// Проверяем кэш
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')
// Сохраняем в кэш на 5 минут
this.cache.set(cacheKey, {
value: decrypted,
expires: Date.now() + 5 * 60 * 1000,
})
// Очищаем старые записи
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)
}
}
}
}
```
### 3. ОБНОВЛЕНИЕ РЕЗОЛВЕРА
```typescript
// В wildberries resolver
import { CryptoService } from '../../../services/crypto.service'
const cryptoService = new CryptoService()
// При сохранении ключа
const encrypted = cryptoService.encrypt(apiKey)
await prisma.apiKey.upsert({
where: {
organizationId_marketplace: {
organizationId: user.organization.id,
marketplace: 'WILDBERRIES',
},
},
update: {
encryptedKey: encrypted.encrypted,
encryptionIv: encrypted.iv,
encryptionTag: encrypted.tag,
isActive: true,
},
create: {
organizationId: user.organization.id,
marketplace: 'WILDBERRIES',
encryptedKey: encrypted.encrypted,
encryptionIv: encrypted.iv,
encryptionTag: encrypted.tag,
isActive: true,
},
})
// При использовании ключа
const apiKeyRecord = await prisma.apiKey.findUnique({
where: {
organizationId_marketplace: {
organizationId: user.organization.id,
marketplace: 'WILDBERRIES',
},
},
})
if (!apiKeyRecord || !apiKeyRecord.isActive) {
return { success: false, message: 'API ключ не найден' }
}
const decryptedKey = cryptoService.decrypt(
apiKeyRecord.encryptedKey,
apiKeyRecord.encryptionIv,
apiKeyRecord.encryptionTag,
)
const wbService = new WildberriesService(decryptedKey)
```
### 4. ОБНОВЛЕНИЕ USER-SETTINGS
```typescript
// В 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,
},
},
})
}
// Обновляем данные пользователя
await apolloClient.refetchQueries({ include: [GET_ME] })
```
### 5. ЗАЩИТА UI
```typescript
// В user-settings.tsx инпуты остаются как есть:
<Input
value={
isEditing
? formData.wildberriesApiKey || ''
: user?.organization?.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES')
? '••••••••••••••••••••' // Показываем звездочки
: ''
}
onChange={(e) => handleInputChange('wildberriesApiKey', e.target.value)}
placeholder="Введите API ключ Wildberries"
readOnly={!isEditing}
className="glass-input"
/>
```
## 📋 ПРОСТОЙ ПЛАН РЕАЛИЗАЦИИ
### ШАГ 1: Подготовка (2 часа)
1. Создать миграцию БД
2. Генерировать ключ шифрования: `openssl rand -hex 32`
3. Добавить в .env: `ENCRYPTION_KEY=сгенерированный_ключ`
### ШАГ 2: Бэкенд (4 часа)
1. Создать CryptoService
2. Обновить резолвер addMarketplaceApiKey
3. Обновить резолвер getWildberriesStatistics
4. Миграция существующих ключей (если есть)
### ШАГ 3: Фронтенд (2 часа)
1. Обновить handleSave в user-settings
2. Проверить что звездочки не копируются
3. Тестирование
## 🔒 БЕЗОПАСНОСТЬ
1. **ENCRYPTION_KEY** - хранить в безопасном месте
2. **Не логировать** расшифрованные ключи
3. **HTTPS обязателен** для передачи ключей
4. **Резервные копии** ключа шифрования
## ✅ ИТОГО
- Простая схема без лишних полей
- Быстрое шифрование с кэшем
- Минимальные изменения в коде
- Сохранение через существующие инпуты
- Защита от просмотра реальных ключей