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

7.7 KiB
Raw Blame History

🔐 УПРОЩЕННЫЙ ПЛАН СИСТЕМЫ API КЛЮЧЕЙ

📋 ТРЕБОВАНИЯ (УТОЧНЕННЫЕ)

  1. Один ключ на маркетплейс для организации
  2. Шифрование всех ключей в БД
  3. Быстрый доступ без задержек
  4. Безопасный UI - только звездочки, не копируются
  5. Простота - минимум сложности

🏗️ АРХИТЕКТУРА

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 часа)

  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. Резервные копии ключа шифрования

ИТОГО

  • Простая схема без лишних полей
  • Быстрое шифрование с кэшем
  • Минимальные изменения в коде
  • Сохранение через существующие инпуты
  • Защита от просмотра реальных ключей