# 🔐 ДЕТАЛЬНЫЙ ПЛАН РЕАЛИЗАЦИИ СИСТЕМЫ 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<string, string>({
    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<ApiKey> {
    // 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<string> {
    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<void> {
    // Валидация нового ключа
    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<ApiKeyDisplay[]>([])
  const [showAddModal, setShowAddModal] = useState(false)
  const [editingKey, setEditingKey] = useState<string | null>(null)

  return (
    <div className="space-y-6">
      {/* Заголовок с кнопкой добавления */}
      <div className="flex justify-between items-center">
        <h3 className="text-lg font-semibold text-white">API Ключи</h3>
        <Button onClick={() => setShowAddModal(true)}>
          <Plus className="h-4 w-4 mr-2" />
          Добавить ключ
        </Button>
      </div>

      {/* Список ключей */}
      <div className="space-y-4">
        {apiKeys.map((apiKey) => (
          <Card key={apiKey.id} className="glass-card p-4">
            <div className="flex items-center justify-between">
              <div className="flex-1">
                <div className="flex items-center gap-3">
                  <h4 className="font-medium text-white">{apiKey.name}</h4>
                  <Badge variant={apiKey.isPrimary ? 'default' : 'secondary'}>
                    {apiKey.isPrimary ? 'Основной' : 'Дополнительный'}
                  </Badge>
                  <Badge variant={apiKey.isActive ? 'success' : 'destructive'}>
                    {apiKey.isActive ? 'Активен' : 'Неактивен'}
                  </Badge>
                </div>

                <div className="mt-2 space-y-1">
                  <p className="text-sm text-white/60">
                    Маркетплейс: {apiKey.marketplace}
                  </p>
                  <p className="text-sm text-white/60">
                    Ключ: {apiKey.maskedKey}
                  </p>
                  {apiKey.lastUsedAt && (
                    <p className="text-sm text-white/60">
                      Использован: {formatDate(apiKey.lastUsedAt)}
                    </p>
                  )}
                </div>
              </div>

              <div className="flex items-center gap-2">
                {editingKey === apiKey.id ? (
                  <RotateKeyForm
                    apiKeyId={apiKey.id}
                    onSuccess={() => setEditingKey(null)}
                    onCancel={() => setEditingKey(null)}
                  />
                ) : (
                  <>
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => setEditingKey(apiKey.id)}
                    >
                      <RefreshCw className="h-4 w-4" />
                    </Button>
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => toggleKeyStatus(apiKey.id)}
                    >
                      {apiKey.isActive ?
                        <ToggleLeft className="h-4 w-4" /> :
                        <ToggleRight className="h-4 w-4" />
                      }
                    </Button>
                  </>
                )}
              </div>
            </div>
          </Card>
        ))}
      </div>

      {/* Модальное окно добавления */}
      {showAddModal && (
        <AddApiKeyModal
          onClose={() => setShowAddModal(false)}
          onSuccess={() => {
            setShowAddModal(false)
            refetchApiKeys()
          }}
        />
      )}
    </div>
  )
}
```

### 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. **Фоновая валидация ключей раз в сутки**
