docs: добавить планы улучшения архитектуры SFERA

This commit is contained in:
Veronika Smirnova
2025-09-18 21:28:07 +03:00
parent 733ccadeb7
commit 3efc387308
18 changed files with 3130 additions and 74 deletions

View File

@ -0,0 +1,488 @@
# 🔐 ПЛАН РЕАЛИЗАЦИИ БЕЗОПАСНОЙ СИСТЕМЫ API КЛЮЧЕЙ
> **Дата:** 2025-09-18
> **Проект:** SFERA
> **Статус:** ГОТОВ К РЕАЛИЗАЦИИ
## 📊 РЕЗУЛЬТАТЫ ДИАГНОСТИКИ
### ✅ ТЕКУЩЕЕ СОСТОЯНИЕ СИСТЕМЫ:
**Данные в БД:**
- 1 активный API ключ Wildberries (длина 398 символов)
- Организация "Rennel" типа SELLER
- Ключ создан: 2025-09-17T15:19:35.423Z
- **ПРОБЛЕМА:** Хранится в открытом виде в поле `apiKey`
**UI Состояние:**
- ✅ Звездочки отображаются корректно: `••••••••••••••••••••`
- ✅ Логика переключения работает (редактирование очищает поле)
- ✅ Индикатор статуса показывает "API ключ настроен"
-**КРИТИЧНО:** handleSave НЕ СОХРАНЯЕТ API ключи
**Безопасность:**
- ❌ Ключи хранятся в открытом виде в БД
- ❌ ENCRYPTION_KEY отсутствует в .env
- ⚠️ Нужно проверить копирование звездочек
### 🎯 ЦЕЛИ РЕАЛИЗАЦИИ:
1. **Безопасность:** Зашифровать существующий API ключ
2. **Функциональность:** Исправить сохранение в handleSave
3. **UX:** Защитить от копирования звездочек
4. **Производительность:** Кэш расшифровки на 2 часа
---
## 🛡️ ПОЭТАПНЫЙ ПЛАН РЕАЛИЗАЦИИ
### 📋 ЭТАП 1: ТЕСТИРОВАНИЕ КОПИРОВАНИЯ (10 мин)
**Цель:** Убедиться что звездочки не раскрывают реальный ключ
**Задачи:**
- [ ] Запустить dev сервер
- [ ] Открыть /seller/settings в браузере
- [ ] Протестировать копирование поля с звездочками
- [ ] При необходимости добавить атрибуты блокировки
**Ожидаемый результат:** Звездочки нельзя скопировать как реальный ключ
**Критерии успеха:**
- ✅ При копировании получаются звездочки, а не настоящий ключ
- ✅ Невозможно выделить реальный ключ из поля
---
### 🔧 ЭТАП 2: ИСПРАВЛЕНИЕ СОХРАНЕНИЯ (30 мин)
**Цель:** handleSave начинает сохранять API ключи в БД
**Подготовка:**
```bash
# Backup текущего состояния
cp src/components/dashboard/user-settings.tsx src/components/dashboard/user-settings.tsx.backup
```
**Задачи:**
- [ ] Найти где обрезана функция handleSave (после строки 698)
- [ ] Добавить импорт ADD_MARKETPLACE_API_KEY в user-settings.tsx
- [ ] Добавить логику сохранения API ключей после сохранения профиля
- [ ] Протестировать сохранение через UI
**Код изменений:**
```typescript
// В импортах добавить:
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN, ADD_MARKETPLACE_API_KEY } from '@/graphql/mutations'
// В 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
}
}
})
}
// Обновляем кэш с новыми API ключами
await apolloClient.refetchQueries({
include: [GET_ME]
})
```
**Тестирование:**
- [ ] Ввести новый WB ключ → должен сохраниться в БД
- [ ] Проверить что ключ виден в GraphQL ответе
- [ ] Проверить работу статистики с новым ключом
**Rollback план:** Восстановить из backup файла
---
### 🔐 ЭТАП 3: СОЗДАНИЕ КРИПТОСЕРВИСА (45 мин)
**Цель:** Безопасное шифрование с кэшем на 2 часа
**Подготовка:**
```bash
# Генерация мастер-ключа
openssl rand -hex 32
# Добавление в .env
echo "ENCRYPTION_KEY=сгенерированный_ключ" >> .env
```
**Задачи:**
- [ ] Создать `/src/services/crypto.service.ts`
- [ ] Реализовать AES-256-GCM шифрование
- [ ] Добавить кэш с TTL 2 часа
- [ ] Написать юнит-тесты
**Реализация CryptoService:**
```typescript
// src/services/crypto.service.ts
import crypto from 'crypto'
export class CryptoService {
private algorithm = 'aes-256-gcm'
private key: Buffer
// Кэш на 2 часа
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 {
// Проверяем кэш (2 часа TTL)
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')
// Сохраняем в кэш на 2 часа
this.cache.set(cacheKey, {
value: decrypted,
expires: Date.now() + 2 * 60 * 60 * 1000 // 2 часа
})
// Очищаем старые записи
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)
}
}
}
}
```
**Тестирование:**
- [ ] Шифрование/расшифровка работает корректно
- [ ] Кэш сохраняет данные на 2 часа
- [ ] Автоочистка удаляет старые записи
---
### 🗄️ ЭТАП 4: МИГРАЦИЯ БД (30 мин)
**Цель:** Добавить поля для зашифрованных ключей
**Задачи:**
- [ ] Создать Prisma миграцию с новыми полями
- [ ] Применить миграцию к БД
- [ ] Проверить совместимость
**Новая схема:**
```prisma
model ApiKey {
id String @id @default(cuid())
marketplace MarketplaceType
// Новые поля для шифрования
encryptedKey String? // Зашифрованный ключ
encryptionIv String? // Вектор инициализации
encryptionTag String? // Тег аутентификации
lastUsedAt DateTime? // Аудит использования
// Старое поле (временно для совместимости)
apiKey String // УДАЛИТЬ после миграции
clientId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
validationData Json?
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
@@unique([organizationId, marketplace])
@@map("api_keys")
}
```
**Команды миграции:**
```bash
npx prisma migrate dev --name add_api_keys_encryption
npx prisma generate
```
**Тестирование:**
- [ ] Миграция применяется без ошибок
- [ ] Существующий ключ остается рабочим
- [ ] Новые поля готовы к использованию
**Rollback:** `npx prisma migrate reset` (данные не критичны)
---
### 🔄 ЭТАП 5: ОБНОВЛЕНИЕ РЕЗОЛВЕРОВ (45 мин)
**Цель:** Использовать шифрование в GraphQL
**Задачи:**
- [ ] Обновить addMarketplaceApiKey для шифрования новых ключей
- [ ] Обновить getWildberriesStatistics для расшифровки
- [ ] Добавить fallback на старые ключи
- [ ] Протестировать API
**Изменения в wildberries.ts:**
```typescript
import { CryptoService } from '../../../services/crypto.service'
const cryptoService = new CryptoService()
// В addMarketplaceApiKey:
if (existing) {
// Шифруем новый ключ
const encrypted = cryptoService.encrypt(apiKey)
await prisma.apiKey.update({
where: { id: existing.id },
data: {
encryptedKey: encrypted.encrypted,
encryptionIv: encrypted.iv,
encryptionTag: encrypted.tag,
apiKey: null, // Очищаем старое поле
isActive: true
}
})
}
// В getWildberriesStatistics:
let decryptedKey: string
if (apiKey.encryptedKey && apiKey.encryptionIv && apiKey.encryptionTag) {
// Новый зашифрованный ключ
decryptedKey = cryptoService.decrypt(
apiKey.encryptedKey,
apiKey.encryptionIv,
apiKey.encryptionTag
)
} else if (apiKey.apiKey) {
// Fallback на старый незашифрованный ключ
decryptedKey = apiKey.apiKey
} else {
return { success: false, message: 'API ключ не найден' }
}
```
**Тестирование:**
- [ ] Новые ключи шифруются при сохранении
- [ ] Старый ключ работает через fallback
- [ ] Статистика загружается корректно
- [ ] Кэш работает (повторные запросы быстрее)
---
### 📦 ЭТАП 6: МИГРАЦИЯ СУЩЕСТВУЮЩЕГО КЛЮЧА (15 мин)
**Цель:** Перешифровать единственный ключ в БД
**Задачи:**
- [ ] Создать скрипт миграции
- [ ] Выполнить перешифровку
- [ ] Убрать fallback логику
- [ ] Финальное тестирование
**Скрипт миграции:**
```javascript
// scripts/migrate-existing-api-keys.js
import { PrismaClient } from '@prisma/client'
import { CryptoService } from '../src/services/crypto.service.js'
const prisma = new PrismaClient()
const cryptoService = new CryptoService()
async function migrateApiKeys() {
const keys = await prisma.apiKey.findMany({
where: {
apiKey: { not: null },
encryptedKey: null
}
})
console.log(`🔄 Найдено ключей для миграции: ${keys.length}`)
for (const key of keys) {
if (!key.apiKey) continue
const encrypted = cryptoService.encrypt(key.apiKey)
await prisma.apiKey.update({
where: { id: key.id },
data: {
encryptedKey: encrypted.encrypted,
encryptionIv: encrypted.iv,
encryptionTag: encrypted.tag,
apiKey: null // Очищаем старое поле
}
})
console.log(`✅ Мигрирован ключ ${key.id} для ${key.marketplace}`)
}
console.log('🎉 Миграция завершена')
}
migrateApiKeys().catch(console.error)
```
**Выполнение:**
```bash
node scripts/migrate-existing-api-keys.js
```
**Тестирование:**
- [ ] Существующий ключ перешифрован
- [ ] Статистика продолжает работать
- [ ] В БД нет незашифрованных ключей
- [ ] Fallback больше не используется
---
## 🧪 ПЛАН ТЕСТИРОВАНИЯ
### После каждого этапа проверять:
**1. Основная функциональность:**
- [ ] Авторизация в кабинет селлера работает
- [ ] Настройки → API ключи открываются
- [ ] Статистика Wildberries загружается
**2. Сохранение ключей:**
- [ ] Ввод нового WB ключа сохраняется
- [ ] Настройки сохраняются без ошибок
- [ ] Ключ появляется в БД
**3. Безопасность:**
- [ ] Звездочки показываются вместо ключей
- [ ] Копирование не раскрывает реальные ключи
- [ ] GraphQL не возвращает расшифрованные ключи
- [ ] Ошибки не логируют реальные ключи
**4. Производительность:**
- [ ] Первый запрос статистики (расшифровка)
- [ ] Повторный запрос (из кэша) быстрее
- [ ] Память не растет при множественных запросах
---
## ⚠️ КРИТИЧЕСКИЕ МОМЕНТЫ
### 🚨 ТОЧКИ ОСТАНОВКИ:
- Статистика перестала загружаться
- Ошибки сохранения в настройках
- Потеря существующего API ключа
- Любые 500 ошибки в GraphQL
- Ошибки шифрования/расшифровки
### 🔄 ROLLBACK ПЛАНЫ:
**Этап 2:** Восстановить user-settings.tsx из backup
**Этап 3:** Не применять к продакшену
**Этап 4:** `npx prisma migrate reset`
**Этап 5:** Откатить изменения резолверов
**Этап 6:** Восстановить из backup БД
### 💾 BACKUP ФАЙЛЫ:
-`api_keys_backup_1758196936364.json` - данные БД
-`user-settings.tsx.backup` - будет создан на этапе 2
---
## 🎯 ОЖИДАЕМЫЙ РЕЗУЛЬТАТ
### После завершения всех этапов:
**✅ Безопасность:**
- API ключи зашифрованы в БД алгоритмом AES-256-GCM
- UI показывает только звездочки
- Копирование не раскрывает реальные ключи
- GraphQL не возвращает расшифрованные ключи
**✅ Функциональность:**
- Сохранение API ключей через UI работает
- Статистика Wildberries загружается корректно
- Существующий ключ мигрирован без потерь
**✅ Производительность:**
- Кэш расшифровки на 2 часа
- Автоочистка старых записей кэша
- Быстрые повторные запросы
**✅ Аудит:**
- Поле lastUsedAt отслеживает использование
- Логи не содержат реальных ключей
- Безопасное хранение в production
---
## 📋 ЧЕКЛИСТ ГОТОВНОСТИ
Перед началом убедиться:
- [ ] Проект запускается локально
- [ ] БД доступна и работает
- [ ] Есть доступ к кабинету селлера
- [ ] Backup данных создан
- [ ] ENCRYPTION_KEY сгенерирован
**План готов к исполнению! 🚀**
---
*Документ создан: 2025-09-18*
*Проект: SFERA Marketplace Platform*
*Статус: Готов к реализации*

View File

@ -0,0 +1,304 @@
# 📋 ПЛАН УЛУЧШЕНИЯ АРХИТЕКТУРЫ АВТОРИЗАЦИИ И БЕЗОПАСНОСТИ SFERA
**Дата создания:** 2025-09-18
**Автор:** Claude AI + Вероника Смирнова
**Статус:** В разработке
## 📊 Анализ текущего состояния
### ✅ Что уже реализовано и работает:
1. **Безопасные ID (CUID)**
- Все модели используют `@default(cuid())`
- Невозможно угадать ID других организаций
- Пример: `cmfpe46iv0001y51d87f4vy2n`
2. **Защита типов кабинетов**
- `useRoleGuard` на 49 страницах
- Автоматический редирект при попытке доступа к чужому типу кабинета
- Селлер не может зайти в кабинет Поставщика
3. **Изоляция данных на уровне API**
- Все резолверы фильтруют по `organizationId`
- Каждая организация видит только свои данные
- Проверки в каждом GraphQL резолвере
4. **Структура роутинга**
- Четкое разделение: `/seller/*`, `/fulfillment/*`, `/logistics/*`, `/wholesale/*`
- DashboardHome автоматически направляет в нужный кабинет
### 🚨 Выявленные проблемы:
1. **Отсутствие глобального состояния (AuthContext)**
- Каждый компонент создает свой экземпляр useAuth
- Состояние не синхронизируется между компонентами
- Sidebar не видит данные пользователя после авторизации
2. **Дублирование кода безопасности**
- Одинаковые проверки в 50+ резолверах
- Риск забыть добавить проверку в новый резолвер
- Сложность поддержки
3. **Отсутствие персистентности**
- При обновлении страницы состояние теряется
- Повторные запросы GET_ME
- Плохой UX при F5
4. **Нет серверной проверки роутов**
- Проверки только на уровне компонентов
- Страница начинает загружаться до проверки прав
## 🎯 План улучшений
### ФАЗА 1: Критические исправления
#### 1. Реализация AuthContext [🔴 КРИТИЧНО]
**Срок:** 2-3 часа
**Приоритет:** Максимальный
**Влияние:** Решает проблему с sidebar и синхронизацией состояния
**План реализации:**
1. Создать `/src/contexts/AuthContext.tsx`:
```typescript
import { createContext, useContext, useState, useEffect } from 'react'
import { useApolloClient } from '@apollo/client'
interface AuthContextType {
user: User | null
isAuthenticated: boolean
isLoading: boolean
checkAuth: () => Promise<void>
login: (phone: string, code: string) => Promise<void>
logout: () => void
}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }) {
// Перенести всю логику из текущего useAuth
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
// ... вся логика авторизации
return (
<AuthContext.Provider value={{ user, ... }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
```
2. Обновить `/src/app/providers.tsx`:
```typescript
export function Providers({ children }) {
return (
<ApolloProvider client={apolloClient}>
<AuthProvider>
<SidebarProvider>
{children}
</SidebarProvider>
</AuthProvider>
</ApolloProvider>
)
}
```
3. Убрать временные решения:
- Удалить `useQuery(GET_ME)` из AppShell
- Убрать передачу user через props
- Вернуть оригинальную логику компонентов
#### 2. GraphQL Middleware для безопасности [🟡 ВАЖНО]
**Срок:** 1 день
**Приоритет:** Высокий
**Влияние:** Централизованная безопасность, чистый код
**План реализации:**
1. Создать `/src/graphql/middleware/organizationAccess.ts`:
```typescript
export const organizationAccessMiddleware = async (
resolve,
parent,
args,
context,
info
) => {
// Пропускаем публичные операции
const PUBLIC_OPERATIONS = ['login', 'sendSmsCode', 'verifySmsCode']
if (PUBLIC_OPERATIONS.includes(info.fieldName)) {
return resolve(parent, args, context, info)
}
// Проверяем авторизацию
if (!context.user?.organizationId) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
// Автоматически добавляем organizationId к запросам
if (args.where && typeof args.where === 'object') {
args.where.organizationId = context.user.organizationId
}
// Логирование для безопасности
console.log(`[${context.user.organizationId}] ${info.fieldName}`, {
userId: context.user.id,
operation: info.fieldName,
timestamp: new Date().toISOString()
})
return resolve(parent, args, context, info)
}
```
2. Применить middleware ко всем резолверам
3. Удалить дублированные проверки из резолверов
### ФАЗА 2: Важные улучшения
#### 3. Персистентность состояния [🟡 ВАЖНО]
**Срок:** 0.5 дня
**Приоритет:** Средний
**Влияние:** Улучшение UX при обновлении страницы
**План реализации:**
1. Добавить в AuthContext сохранение состояния:
```typescript
// При успешной авторизации
const encryptedUser = encrypt(JSON.stringify(user))
localStorage.setItem('auth:user', encryptedUser)
// При инициализации
const savedUser = localStorage.getItem('auth:user')
if (savedUser) {
const user = JSON.parse(decrypt(savedUser))
// Проверить валидность токена
}
```
2. Реализовать refresh token механизм
3. Очистка при logout
#### 4. Next.js Middleware [🟢 ЖЕЛАТЕЛЬНО]
**Срок:** 1 день
**Приоритет:** Средний
**Влияние:** Производительность, серверная защита
**План реализации:**
1. Создать `/src/middleware.ts`:
```typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
const pathname = request.nextUrl.pathname
// Публичные маршруты
const publicPaths = ['/', '/login', '/register']
if (publicPaths.includes(pathname)) return
// Проверка токена
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
const { payload } = await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET!))
const orgType = payload.organization?.type
// Проверка доступа к кабинету
if (pathname.startsWith('/seller') && orgType !== 'SELLER') {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
// ... остальные проверки
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
```
### ФАЗА 3: Оптимизации
#### 5. Улучшение загрузки [🔵 ОПЦИОНАЛЬНО]
**Срок:** 1 неделя
**Приоритет:** Низкий
**Влияние:** Улучшение UX
- Skeleton screens для всех страниц
- Prefetch критических данных
- Оптимистичные обновления UI
- PWA функциональность
#### 6. Расширенная система прав [🔵 ОПЦИОНАЛЬНО]
**Срок:** 2 недели
**Приоритет:** Низкий
**Влияние:** Enterprise функциональность
- Роли: Admin, Manager, Viewer
- Permissions: canEdit, canDelete, canApprove
- Аудит всех действий
- Делегирование доступа
## 📈 Ожидаемые результаты
### После ФАЗЫ 1:
- ✅ Sidebar работает стабильно
- ✅ Состояние синхронизировано между компонентами
- ✅ Единая точка контроля безопасности
- ✅ Чистый, поддерживаемый код
### После ФАЗЫ 2:
- ✅ Мгновенная загрузка после F5
- ✅ Защита на уровне сервера
- ✅ Автоматическое управление сессией
### После ФАЗЫ 3:
- ✅ Премиум UX
- ✅ Enterprise-ready система
- ✅ Готовность к масштабированию
## 🚀 Рекомендуемый порядок выполнения
1. **Неделя 1:** AuthContext (решает критическую проблему)
2. **Неделя 2:** GraphQL Middleware + Персистентность
3. **Месяц 2:** Next.js Middleware + Оптимизации по необходимости
## 📝 Критерии успеха
- [ ] Sidebar отображается сразу после авторизации
- [ ] Нет дублирования состояния между компонентами
- [ ] Единая проверка безопасности для всех API запросов
- [ ] Состояние сохраняется при обновлении страницы
- [ ] Производительность не ухудшилась
- [ ] Код стал чище и проще в поддержке
---
*Документ будет обновляться по мере реализации плана*

View File

@ -0,0 +1,505 @@
# 🔐 ПЛАН БЕЗОПАСНОЙ МИГРАЦИИ НА AuthContext
**Дата создания:** 2025-09-18
**Автор:** Claude AI + Вероника Смирнова
**Статус:** Готов к реализации
## 📊 Результаты глубокой диагностики
### Анализ текущей реализации useAuth
**Размер и сложность:**
- 657 строк кода
- 65 файлов используют useAuth()
- Сложная логика с rollback механизмами
- Интеграция с Apollo Client через localStorage
**Ключевые компоненты:**
1. **State Management**
- `user: User | null` - данные пользователя
- `isAuthenticated: boolean` - статус авторизации
- `isLoading: boolean` - индикатор загрузки
- `isCheckingAuth: boolean` - защита от дублирования
2. **Методы авторизации**
- `sendSmsCode` - отправка SMS
- `verifySmsCode` - проверка кода
- `checkAuth` - проверка текущей сессии
- `logout` - выход
3. **Методы регистрации**
- `registerFulfillmentOrganization`
- `registerSellerOrganization`
- `registerOrganization` (универсальный)
4. **Интеграции**
- Apollo Client для GraphQL
- localStorage для токенов
- refreshApolloClient для синхронизации
### Выявленные проблемы
1. **Множественные экземпляры состояния**
```
AppShell → useAuth() → useState (копия 1)
Sidebar → useAuth() → useState (копия 2)
Component → useAuth() → useState (копия 3)
```
2. **Race conditions**
- checkAuth вызывается параллельно из разных компонентов
- isCheckingAuth защищает только локальный экземпляр
3. **Отсутствие синхронизации**
- Обновления в одном компоненте не видны в других
- GET_ME выполняется многократно
4. **Проблемы с SSR**
- Прямое обращение к localStorage
- window checks разбросаны по коду
## 🎯 Архитектура решения
### Новая структура с AuthContext
```
AuthProvider (глобальное состояние)
├── Apollo Provider
│ └── Auth Link (токены из контекста)
├── State Management
│ ├── user
│ ├── isAuthenticated
│ └── isLoading
└── Methods
├── Authentication
├── Registration
└── Session Management
```
### Преимущества
1. **Единое состояние** - все компоненты видят одни данные
2. **Оптимизация запросов** - GET_ME выполняется 1 раз
3. **Синхронизация** - изменения видны везде мгновенно
4. **SSR совместимость** - централизованные проверки
5. **Типобезопасность** - строгая типизация контекста
## 📋 Поэтапный план миграции
### ЭТАП 0: Подготовка [30 мин]
**Цель:** Создать безопасную среду для миграции
1. **Создать backup текущего состояния**
```bash
git add .
git commit -m "backup: перед миграцией на AuthContext"
git branch backup-before-auth-context
```
2. **Создать feature branch**
```bash
git checkout -b feature/auth-context-migration
```
3. **Подготовить структуру папок**
```
src/
├── contexts/
│ └── auth/
│ ├── AuthContext.tsx # Основной контекст
│ ├── AuthProvider.tsx # Provider компонент
│ ├── types.ts # TypeScript типы
│ └── utils.ts # Вспомогательные функции
```
### ЭТАП 1: Создание AuthContext с минимальной функциональностью [1 час]
**Цель:** Создать работающий контекст без нарушения существующего функционала
1. **Создать типы** (`src/contexts/auth/types.ts`)
```typescript
export interface User {
id: string
phone: string
avatar?: string
managerName?: string
organization?: Organization
}
export interface Organization {
id: string
inn: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
// ... остальные поля
}
export interface AuthState {
user: User | null
isAuthenticated: boolean
isLoading: boolean
}
export interface AuthContextType extends AuthState {
// Методы будем добавлять постепенно
checkAuth: () => Promise<void>
logout: () => void
}
```
2. **Создать контекст** (`src/contexts/auth/AuthContext.tsx`)
```typescript
import { createContext } from 'react'
import type { AuthContextType } from './types'
export const AuthContext = createContext<AuthContextType | null>(null)
```
3. **Создать базовый Provider** (`src/contexts/auth/AuthProvider.tsx`)
```typescript
import { useState, useCallback, useEffect } from 'react'
import { AuthContext } from './AuthContext'
import { getAuthToken, removeAuthToken } from '@/lib/apollo-client'
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLoading, setIsLoading] = useState(true)
// Минимальная реализация checkAuth
const checkAuth = useCallback(async () => {
const token = getAuthToken()
if (!token) {
setIsAuthenticated(false)
setUser(null)
setIsLoading(false)
return
}
// TODO: Добавить GET_ME запрос
setIsLoading(false)
}, [])
// Минимальная реализация logout
const logout = useCallback(() => {
removeAuthToken()
setUser(null)
setIsAuthenticated(false)
window.location.href = '/'
}, [])
useEffect(() => {
checkAuth()
}, [checkAuth])
const value = {
user,
isAuthenticated,
isLoading,
checkAuth,
logout
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
```
4. **Создать временный хук-обертку** (`src/hooks/useAuth.ts`)
```typescript
// В начале файла добавляем
import { useContext } from 'react'
import { AuthContext } from '@/contexts/auth/AuthContext'
// Временный флаг для постепенной миграции
const USE_AUTH_CONTEXT = false
export const useAuth = (): UseAuthReturn => {
if (USE_AUTH_CONTEXT) {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
// Адаптер для совместимости API
return {
...context,
// Методы-заглушки для совместимости
sendSmsCode: async () => ({ success: false, message: 'Not implemented' }),
verifySmsCode: async () => ({ success: false, message: 'Not implemented' }),
registerFulfillmentOrganization: async () => ({ success: false, message: 'Not implemented' }),
registerSellerOrganization: async () => ({ success: false, message: 'Not implemented' }),
registerOrganization: async () => ({ success: false, message: 'Not implemented' }),
updateUser: () => {}
}
}
// Существующая реализация остается без изменений
// ... весь текущий код
}
```
### ЭТАП 2: Тестирование базовой интеграции [30 мин]
**Цель:** Убедиться что ничего не сломалось
1. **Добавить AuthProvider в providers.tsx**
```typescript
import { AuthProvider } from '@/contexts/auth/AuthProvider'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ApolloProvider client={apolloClient}>
<AuthProvider>
<SidebarProvider>
{children}
</SidebarProvider>
</AuthProvider>
</ApolloProvider>
)
}
```
2. **Включить USE_AUTH_CONTEXT для одного компонента**
- Начать с простого компонента (например, UserProfile в sidebar)
- Проверить что компонент рендерится
- Проверить что нет ошибок в консоли
3. **Rollback план**
- Если есть ошибки - установить USE_AUTH_CONTEXT = false
- Исправить проблемы
- Повторить тестирование
### ЭТАП 3: Миграция основного функционала [2 часа]
**Цель:** Перенести всю логику в AuthContext
1. **Перенести checkAuth с GET_ME**
```typescript
const checkAuth = useCallback(async () => {
if (isCheckingAuth.current) return
const token = getAuthToken()
if (!token) {
setIsAuthenticated(false)
setUser(null)
setIsLoading(false)
return
}
isCheckingAuth.current = true
setIsLoading(true)
try {
const { data } = await apolloClient.query({
query: GET_ME,
errorPolicy: 'all',
fetchPolicy: 'network-only'
})
if (data?.me) {
setUser(data.me)
setIsAuthenticated(true)
setUserData(data.me)
}
} catch (error) {
// Обработка ошибок
} finally {
isCheckingAuth.current = false
setIsLoading(false)
}
}, [])
```
2. **Перенести SMS методы**
- sendSmsCode
- verifySmsCode
- Сохранить всю логику с логированием
3. **Перенести методы регистрации**
- registerFulfillmentOrganization
- registerSellerOrganization
- registerOrganization
- Сохранить rollback механизмы
4. **Добавить updateUser**
```typescript
const updateUser = useCallback((updatedUser: Partial<User>) => {
setUser(current => {
if (!current) return current
const updated = { ...current, ...updatedUser }
setUserData(updated) // Синхронизация с localStorage
return updated
})
}, [])
```
### ЭТАП 4: Постепенная миграция компонентов [1 день]
**Цель:** Безопасно перевести все компоненты на новую систему
1. **Приоритетные компоненты** (первая очередь)
- AppShell
- Sidebar и все его варианты
- AuthGuard
2. **Критические компоненты** (вторая очередь)
- Страницы авторизации (login, register)
- DashboardHome
- useRoleGuard
3. **Остальные компоненты** (третья очередь)
- Разбить на группы по 10-15 файлов
- Мигрировать группами
- Тестировать после каждой группы
**Процесс для каждого компонента:**
1. Включить USE_AUTH_CONTEXT локально
2. Проверить функциональность
3. Если работает - коммит
4. Если нет - откат и исправление
### ЭТАП 5: Оптимизация и очистка [1 час]
**Цель:** Удалить старый код и оптимизировать
1. **Удалить старую реализацию из useAuth**
- Оставить только обертку для контекста
- Удалить локальные useState
- Удалить дублированную логику
2. **Оптимизировать рендеринг**
```typescript
// Мемоизация значения контекста
const value = useMemo(() => ({
user,
isAuthenticated,
isLoading,
checkAuth,
logout,
// ... другие методы
}), [user, isAuthenticated, isLoading])
```
3. **Добавить DevTools**
```typescript
if (process.env.NODE_ENV === 'development') {
(window as any).__AUTH_STATE__ = { user, isAuthenticated }
}
```
### ЭТАП 6: Финальное тестирование [1 час]
**Цель:** Убедиться что все работает
1. **Функциональные тесты**
- [ ] Авторизация по SMS
- [ ] Регистрация всех типов организаций
- [ ] Отображение sidebar
- [ ] Переходы между страницами
- [ ] Выход из системы
- [ ] Обновление страницы (F5)
2. **Тесты производительности**
- [ ] Нет множественных GET_ME запросов
- [ ] Нет лишних ре-рендеров
- [ ] Быстрая загрузка после F5
3. **Регрессионные тесты**
- [ ] Все 65 компонентов работают
- [ ] API ключи сохраняются
- [ ] Роутинг по типам организаций
## 🚨 Риски и митигация
### Риск 1: Поломка авторизации
**Митигация:**
- Постепенная миграция через флаг USE_AUTH_CONTEXT
- Возможность быстрого отката
- Тестирование на каждом этапе
### Риск 2: Потеря состояния
**Митигация:**
- Сохранение в localStorage остается
- Rollback механизмы сохраняются
- Логирование всех изменений
### Риск 3: Проблемы с SSR
**Митигация:**
- Все проверки window в одном месте
- useEffect для клиентских операций
- Правильная инициализация состояния
### Риск 4: Race conditions
**Митигация:**
- useRef для флагов загрузки
- Отмена дублированных запросов
- Правильная очередность операций
## 📊 Метрики успеха
1. **Функциональность**
- ✅ Все 65 компонентов работают
- ✅ Sidebar отображается корректно
- ✅ Авторизация стабильна
2. **Производительность**
- ✅ GET_ME вызывается 1 раз
- ✅ Нет задержек при навигации
- ✅ Быстрая загрузка после F5
3. **Качество кода**
- ✅ Единое место управления состоянием
- ✅ Типобезопасность
- ✅ Отсутствие дублирования
## 🔄 Rollback план
Если что-то пойдет не так на любом этапе:
1. **Быстрый откат**
```bash
git checkout backup-before-auth-context
```
2. **Частичный откат**
- Установить USE_AUTH_CONTEXT = false
- Вернуть проблемные компоненты на старую версию
- Исправить проблемы в изолированной ветке
3. **Восстановление данных**
- localStorage сохраняется
- Токены остаются валидными
- Пользователи не заметят проблем
## 📝 Чек-лист готовности
Перед началом миграции убедитесь:
- [ ] Создан backup текущего состояния
- [ ] Команда предупреждена о работах
- [ ] Подготовлен план коммуникации при проблемах
- [ ] Есть доступ к логам и мониторингу
- [ ] Определено время для миграции (лучше в период низкой активности)
## 🎯 Ожидаемый результат
После завершения миграции:
1. **Немедленные улучшения**
- Sidebar работает стабильно
- Состояние синхронизировано между компонентами
- Уменьшено количество запросов к API
2. **Долгосрочные преимущества**
- Готовность к добавлению новых функций
- Упрощенная отладка
- Лучшая производительность
- Возможность добавления продвинутых функций (персистентность, refresh tokens)
---
*Документ будет обновляться по ходу выполнения миграции*