# 🔐 ДЕТАЛЬНЫЙ ПЛАН РЕАЛИЗАЦИИ СИСТЕМЫ API КЛЮЧЕЙ ## 📋 ТРЕБОВАНИЯ 1. **Множественные ключи** - организация может иметь несколько API ключей для разных приложений/магазинов 2. **Шифрование** - все ключи хранятся зашифрованными 3. **Функциональность** - ключи должны работать без задержек на расшифровку 4. **Безопасный UI** - никогда не показывать реальные ключи в интерфейсе 5. **Аудит** - логирование всех операций с ключами ## 🏗️ АРХИТЕКТУРА ### 1. НОВАЯ СХЕМА БД ```prisma // Основная таблица API ключей model ApiKey { id String @id @default(cuid()) name String // Название ключа (например: "Основной магазин WB") marketplace MarketplaceType encryptedKey String // Зашифрованный ключ encryptionIv String // Вектор инициализации encryptionTag String // Тег аутентификации keyFingerprint String // Последние 4 символа для идентификации isActive Boolean @default(true) isPrimary Boolean @default(false) // Основной ключ для маркетплейса // Метаданные clientId String? // Для Ozon validationData Json? // Результаты последней валидации lastValidatedAt DateTime? lastUsedAt DateTime? usageCount Int @default(0) // Связи organizationId String organization Organization @relation(fields: [organizationId], references: [id]) createdById String createdBy User @relation("ApiKeyCreator", fields: [createdById], references: [id]) // Аудит auditLogs ApiKeyAudit[] // Временные метки createdAt DateTime @default(now()) updatedAt DateTime @updatedAt expiresAt DateTime? // Опциональный срок действия @@index([organizationId, marketplace]) @@index([keyFingerprint]) @@map("api_keys") } // Аудит операций с ключами model ApiKeyAudit { id String @id @default(cuid()) action ApiKeyAction performedAt DateTime @default(now()) performedBy String user User @relation(fields: [performedBy], references: [id]) apiKeyId String apiKey ApiKey @relation(fields: [apiKeyId], references: [id]) // Контекст операции ipAddress String? userAgent String? metadata Json? // Дополнительные данные @@index([apiKeyId]) @@index([performedBy]) @@map("api_key_audit") } enum ApiKeyAction { CREATED UPDATED VALIDATED ACTIVATED DEACTIVATED DELETED USED_FOR_SYNC VALIDATION_FAILED } ``` ### 2. СЕРВИС ШИФРОВАНИЯ ```typescript // src/services/encryption-service.ts import crypto from 'crypto' import { LRUCache } from 'lru-cache' export class EncryptionService { private algorithm = 'aes-256-gcm' private keyDerivationSalt: Buffer // Кэш для расшифрованных ключей (время жизни: 5 минут) private decryptedCache = new LRUCache({ max: 100, ttl: 1000 * 60 * 5, // 5 минут dispose: (value) => { // Очистка из памяти if (value) { crypto.randomFillSync(Buffer.from(value)) } }, }) constructor() { const masterKey = process.env.ENCRYPTION_MASTER_KEY if (!masterKey) { throw new Error('ENCRYPTION_MASTER_KEY not set') } // Генерация ключа шифрования из мастер-ключа this.keyDerivationSalt = Buffer.from(process.env.ENCRYPTION_SALT || 'sfera-api-keys-salt') } private getDerivedKey(): Buffer { const masterKey = process.env.ENCRYPTION_MASTER_KEY! return crypto.pbkdf2Sync(masterKey, this.keyDerivationSalt, 10000, 32, 'sha256') } encrypt(plainText: string): EncryptedData { const key = this.getDerivedKey() const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv(this.algorithm, key, iv) let encrypted = cipher.update(plainText, 'utf8', 'hex') encrypted += cipher.final('hex') return { encrypted, iv: iv.toString('hex'), tag: cipher.getAuthTag().toString('hex'), } } decrypt(encryptedData: EncryptedData): string { // Проверяем кэш const cacheKey = `${encryptedData.encrypted}-${encryptedData.iv}` const cached = this.decryptedCache.get(cacheKey) if (cached) { return cached } const key = this.getDerivedKey() const decipher = crypto.createDecipheriv(this.algorithm, key, Buffer.from(encryptedData.iv, 'hex')) decipher.setAuthTag(Buffer.from(encryptedData.tag, 'hex')) let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') // Сохраняем в кэш this.decryptedCache.set(cacheKey, decrypted) return decrypted } // Получение отпечатка ключа (последние 4 символа) getFingerprint(apiKey: string): string { return apiKey.slice(-4) } // Генерация маски для UI generateMask(fingerprint: string): string { return `••••••••••••${fingerprint}` } } interface EncryptedData { encrypted: string iv: string tag: string } ``` ### 3. ОБНОВЛЕННЫЙ СЕРВИС API КЛЮЧЕЙ ```typescript // src/services/api-key-service.ts export class ApiKeyService { constructor( private prisma: PrismaClient, private encryption: EncryptionService, private marketplace: MarketplaceService, ) {} async createApiKey(data: CreateApiKeyInput, userId: string): Promise { // 1. Валидация ключа const validation = await this.marketplace.validateApiKey(data.marketplace, data.apiKey, data.clientId) if (!validation.isValid) { throw new Error(`Недействительный API ключ: ${validation.error}`) } // 2. Шифрование const encrypted = this.encryption.encrypt(data.apiKey) const fingerprint = this.encryption.getFingerprint(data.apiKey) // 3. Сохранение return this.prisma.$transaction(async (tx) => { // Если это первый ключ для маркетплейса - делаем его основным const existingCount = await tx.apiKey.count({ where: { organizationId: data.organizationId, marketplace: data.marketplace, isActive: true, }, }) const apiKey = await tx.apiKey.create({ data: { name: data.name, marketplace: data.marketplace, encryptedKey: encrypted.encrypted, encryptionIv: encrypted.iv, encryptionTag: encrypted.tag, keyFingerprint: fingerprint, clientId: data.clientId, isPrimary: existingCount === 0, organizationId: data.organizationId, createdById: userId, validationData: validation.data, lastValidatedAt: new Date(), }, }) // 4. Аудит await tx.apiKeyAudit.create({ data: { action: 'CREATED', apiKeyId: apiKey.id, performedBy: userId, metadata: { name: data.name, marketplace: data.marketplace, }, }, }) return apiKey }) } async getDecryptedKey(apiKeyId: string, userId: string): Promise { const apiKey = await this.prisma.apiKey.findUnique({ where: { id: apiKeyId }, }) if (!apiKey) { throw new Error('API ключ не найден') } // Аудит использования await this.prisma.apiKeyAudit.create({ data: { action: 'USED_FOR_SYNC', apiKeyId: apiKey.id, performedBy: userId, }, }) // Обновляем статистику await this.prisma.apiKey.update({ where: { id: apiKeyId }, data: { lastUsedAt: new Date(), usageCount: { increment: 1 }, }, }) // Расшифровка return this.encryption.decrypt({ encrypted: apiKey.encryptedKey, iv: apiKey.encryptionIv, tag: apiKey.encryptionTag, }) } async rotateKey(apiKeyId: string, newApiKey: string, userId: string): Promise { // Валидация нового ключа const apiKey = await this.prisma.apiKey.findUnique({ where: { id: apiKeyId }, }) if (!apiKey) throw new Error('API ключ не найден') const validation = await this.marketplace.validateApiKey(apiKey.marketplace, newApiKey, apiKey.clientId) if (!validation.isValid) { throw new Error('Новый ключ недействителен') } // Шифрование нового ключа const encrypted = this.encryption.encrypt(newApiKey) const fingerprint = this.encryption.getFingerprint(newApiKey) // Обновление await this.prisma.$transaction(async (tx) => { await tx.apiKey.update({ where: { id: apiKeyId }, data: { encryptedKey: encrypted.encrypted, encryptionIv: encrypted.iv, encryptionTag: encrypted.tag, keyFingerprint: fingerprint, lastValidatedAt: new Date(), }, }) await tx.apiKeyAudit.create({ data: { action: 'UPDATED', apiKeyId: apiKeyId, performedBy: userId, metadata: { reason: 'key_rotation' }, }, }) }) // Очистка кэша this.encryption.clearCache() } } ``` ### 4. ОБНОВЛЕННЫЙ UI КОМПОНЕНТ ```typescript // src/components/settings/ApiKeysTab.tsx export function ApiKeysTab() { const [apiKeys, setApiKeys] = useState([]) const [showAddModal, setShowAddModal] = useState(false) const [editingKey, setEditingKey] = useState(null) return (
{/* Заголовок с кнопкой добавления */}

API Ключи

{/* Список ключей */}
{apiKeys.map((apiKey) => (

{apiKey.name}

{apiKey.isPrimary ? 'Основной' : 'Дополнительный'} {apiKey.isActive ? 'Активен' : 'Неактивен'}

Маркетплейс: {apiKey.marketplace}

Ключ: {apiKey.maskedKey}

{apiKey.lastUsedAt && (

Использован: {formatDate(apiKey.lastUsedAt)}

)}
{editingKey === apiKey.id ? ( setEditingKey(null)} onCancel={() => setEditingKey(null)} /> ) : ( <> )}
))}
{/* Модальное окно добавления */} {showAddModal && ( setShowAddModal(false)} onSuccess={() => { setShowAddModal(false) refetchApiKeys() }} /> )}
) } ``` ### 5. GRAPHQL СХЕМА ```graphql type ApiKey { id: ID! name: String! marketplace: MarketplaceType! maskedKey: String! # Только маска ••••••••1234 isActive: Boolean! isPrimary: Boolean! lastUsedAt: DateTime lastValidatedAt: DateTime createdAt: DateTime! usageCount: Int! } input CreateApiKeyInput { name: String! marketplace: MarketplaceType! apiKey: String! clientId: String # Для Ozon } type Mutation { createApiKey(input: CreateApiKeyInput!): ApiKeyResponse! updateApiKey(id: ID!, newKey: String!): ApiKeyResponse! toggleApiKeyStatus(id: ID!): ApiKeyResponse! deleteApiKey(id: ID!): Boolean! setPrimaryApiKey(id: ID!): ApiKeyResponse! } type Query { myApiKeys(marketplace: MarketplaceType): [ApiKey!]! validateApiKey(marketplace: MarketplaceType!, apiKey: String!): ValidationResponse! } ``` ## 📋 ПОШАГОВЫЙ ПЛАН РЕАЛИЗАЦИИ ### ЭТАП 1: Подготовка (1 день) 1. ✅ Создать миграцию БД с новой схемой 2. ✅ Настроить переменные окружения для шифрования 3. ✅ Создать EncryptionService 4. ✅ Написать тесты для шифрования ### ЭТАП 2: Бэкенд (2-3 дня) 1. ✅ Создать ApiKeyService 2. ✅ Обновить GraphQL схему 3. ✅ Создать резолверы для API ключей 4. ✅ Добавить middleware для аудита 5. ✅ Миграция существующих ключей ### ЭТАП 3: Интеграция (2 дня) 1. ✅ Обновить WildberriesService для работы с новой системой 2. ✅ Обновить статистику селлеров 3. ✅ Добавить автоматическую ротацию ключей 4. ✅ Настроить мониторинг использования ### ЭТАП 4: UI (2 дня) 1. ✅ Создать компонент ApiKeysTab 2. ✅ Добавить модалки для добавления/редактирования 3. ✅ Интегрировать в user-settings 4. ✅ Добавить уведомления ### ЭТАП 5: Тестирование (1 день) 1. ✅ E2E тесты 2. ✅ Тесты безопасности 3. ✅ Нагрузочное тестирование ## 🔒 МЕРЫ БЕЗОПАСНОСТИ 1. **Никогда не логировать расшифрованные ключи** 2. **Автоматическая очистка кэша каждые 5 минут** 3. **Принудительная ротация ключей каждые 90 дней** 4. **Алерты при подозрительной активности** 5. **Ограничение количества ключей на организацию** ## ⚡ ОПТИМИЗАЦИИ 1. **LRU кэш для расшифрованных ключей** (5 минут TTL) 2. **Индексы БД по organizationId и marketplace** 3. **Batch загрузка ключей для статистики** 4. **Фоновая валидация ключей раз в сутки**