
- 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>
7.7 KiB
7.7 KiB
🔐 УПРОЩЕННЫЙ ПЛАН СИСТЕМЫ API КЛЮЧЕЙ
📋 ТРЕБОВАНИЯ (УТОЧНЕННЫЕ)
- Один ключ на маркетплейс для организации
- Шифрование всех ключей в БД
- Быстрый доступ без задержек
- Безопасный UI - только звездочки, не копируются
- Простота - минимум сложности
🏗️ АРХИТЕКТУРА
1. ПРОСТАЯ СХЕМА БД
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. ПРОСТОЙ СЕРВИС ШИФРОВАНИЯ
// 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. ОБНОВЛЕНИЕ РЕЗОЛВЕРА
// В 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
// В 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
// В 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 часа)
- Создать миграцию БД
- Генерировать ключ шифрования:
openssl rand -hex 32
- Добавить в .env:
ENCRYPTION_KEY=сгенерированный_ключ
ШАГ 2: Бэкенд (4 часа)
- Создать CryptoService
- Обновить резолвер addMarketplaceApiKey
- Обновить резолвер getWildberriesStatistics
- Миграция существующих ключей (если есть)
ШАГ 3: Фронтенд (2 часа)
- Обновить handleSave в user-settings
- Проверить что звездочки не копируются
- Тестирование
🔒 БЕЗОПАСНОСТЬ
- ENCRYPTION_KEY - хранить в безопасном месте
- Не логировать расшифрованные ключи
- HTTPS обязателен для передачи ключей
- Резервные копии ключа шифрования
✅ ИТОГО
- Простая схема без лишних полей
- Быстрое шифрование с кэшем
- Минимальные изменения в коде
- Сохранение через существующие инпуты
- Защита от просмотра реальных ключей