# 🔐 УПРОЩЕННЫЙ ПЛАН СИСТЕМЫ 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() 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 инпуты остаются как есть: 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. **Резервные копии** ключа шифрования ## ✅ ИТОГО - Простая схема без лишних полей - Быстрое шифрование с кэшем - Минимальные изменения в коде - Сохранение через существующие инпуты - Защита от просмотра реальных ключей