feat: migrate from useAuth to AuthContext for centralized auth state
• Полная миграция 64 компонентов с useAuth на AuthContext • Исправлена race condition в SMS регистрации • Улучшена SSR совместимость с таймаутами • Удалена дублирующая система регистрации • Обновлена документация архитектуры аутентификации Технические изменения: - AuthContext.tsx: централизованная система состояния - auth-flow.tsx: убрана агрессивная логика logout - confirmation-step.tsx: исправлена передача телефона - page.tsx: добавлена синхронизация состояния - 64 файла: миграция useAuth → useAuthContext 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
305
docs/presentation-layer/AUTHENTICATION_ARCHITECTURE.md
Normal file
305
docs/presentation-layer/AUTHENTICATION_ARCHITECTURE.md
Normal file
@ -0,0 +1,305 @@
|
||||
# АРХИТЕКТУРА АУТЕНТИФИКАЦИИ SFERA
|
||||
|
||||
**Дата последнего обновления:** 19 сентября 2025
|
||||
**Статус:** ✅ АКТУАЛЬНО (после миграции useAuth → AuthContext)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 ТЕКУЩАЯ АРХИТЕКТУРА
|
||||
|
||||
### AuthContext - Централизованное управление аутентификацией
|
||||
|
||||
**Файл:** `src/contexts/AuthContext.tsx`
|
||||
|
||||
```typescript
|
||||
interface AuthContextType {
|
||||
// Состояние пользователя
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
|
||||
// Методы аутентификации
|
||||
sendSmsCode: (phone: string) => Promise<any>
|
||||
verifySmsCode: (phone: string, code: string) => Promise<any>
|
||||
registerOrganization: (input: any) => Promise<any>
|
||||
checkAuth: () => Promise<void>
|
||||
logout: () => void
|
||||
}
|
||||
```
|
||||
|
||||
### Использование в компонентах
|
||||
|
||||
```typescript
|
||||
import { useAuthContext } from '@/contexts/AuthContext'
|
||||
|
||||
export function MyComponent() {
|
||||
const { user, isAuthenticated, logout } = useAuthContext()
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <LoginForm />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Добро пожаловать, {user?.organization?.name}!</p>
|
||||
<button onClick={logout}>Выход</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ СТРУКТУРА ПРОВАЙДЕРА
|
||||
|
||||
### AuthProvider в app/providers.tsx
|
||||
|
||||
```typescript
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ApolloProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**🔑 Ключевая особенность:** AuthProvider оборачивает все приложение, обеспечивая единое состояние аутентификации.
|
||||
|
||||
---
|
||||
|
||||
## 📋 ТИПЫ ПОЛЬЗОВАТЕЛЕЙ И ОРГАНИЗАЦИЙ
|
||||
|
||||
### User Interface
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
phone: string
|
||||
avatar?: string
|
||||
managerName?: string
|
||||
createdAt?: string
|
||||
organization?: {
|
||||
id: string
|
||||
inn: string
|
||||
kpp?: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
address?: string
|
||||
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
|
||||
referralPoints?: number
|
||||
apiKeys?: ApiKey[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Типы организаций
|
||||
|
||||
- **FULFILLMENT** - фулфилмент центры
|
||||
- **SELLER** - продавцы на маркетплейсах
|
||||
- **LOGIST** - логистические компании
|
||||
- **WHOLESALE** - оптовые поставщики
|
||||
|
||||
---
|
||||
|
||||
## 🔄 ПРОЦЕСС АУТЕНТИФИКАЦИИ
|
||||
|
||||
### 1. SMS Авторизация
|
||||
|
||||
```typescript
|
||||
// Отправка SMS кода
|
||||
const result = await sendSmsCode(phone)
|
||||
|
||||
// Подтверждение кода
|
||||
const verification = await verifySmsCode(phone, code)
|
||||
```
|
||||
|
||||
### 2. Регистрация организации
|
||||
|
||||
```typescript
|
||||
const registrationResult = await registerOrganization({
|
||||
organizationData: {
|
||||
inn: '1234567890',
|
||||
phone: '+7900123456',
|
||||
type: 'SELLER',
|
||||
wbApiKey: 'wb_token',
|
||||
ozonApiKey: 'ozon_token'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Проверка авторизации
|
||||
|
||||
```typescript
|
||||
// Автоматически вызывается при инициализации
|
||||
await checkAuth()
|
||||
|
||||
// Проверяет токен и загружает данные пользователя
|
||||
// Если токен невалиден - выполняет logout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 МАРШРУТИЗАЦИЯ ПО РОЛЯМ
|
||||
|
||||
### Главная страница (app/page.tsx)
|
||||
|
||||
```typescript
|
||||
export default function Home() {
|
||||
const { user, isLoading, isAuthenticated } = useAuthContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return // Ждем загрузки
|
||||
|
||||
if (user?.organization) {
|
||||
router.replace('/dashboard') // Авторизованный пользователь
|
||||
} else if (isAuthenticated && user && !user.organization) {
|
||||
router.replace('/register') // Продолжить регистрацию
|
||||
} else {
|
||||
router.replace('/login') // Неавторизованный
|
||||
}
|
||||
}, [user, isLoading, isAuthenticated])
|
||||
}
|
||||
```
|
||||
|
||||
### Защищенные маршруты (AuthGuard)
|
||||
|
||||
```typescript
|
||||
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated, isLoading } = useAuthContext()
|
||||
|
||||
if (isLoading) return <LoadingScreen />
|
||||
if (!isAuthenticated) return <LoginPage />
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 ИНТЕГРАЦИЯ С GRAPHQL
|
||||
|
||||
### Apollo Client
|
||||
|
||||
AuthContext автоматически:
|
||||
- Устанавливает токены в Apollo Client
|
||||
- Обрабатывает UNAUTHENTICATED ошибки
|
||||
- Синхронизирует состояние с сервером
|
||||
|
||||
```typescript
|
||||
// При логине
|
||||
setAuthToken(token)
|
||||
setUserData(userData)
|
||||
|
||||
// При logout
|
||||
removeAuthToken()
|
||||
apolloClient.resetStore()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 СОСТОЯНИЕ ЗАГРУЗКИ
|
||||
|
||||
### Флаги состояния
|
||||
|
||||
- **isLoading** - идет проверка аутентификации
|
||||
- **isAuthenticated** - пользователь авторизован
|
||||
- **user** - данные пользователя (null если не авторизован)
|
||||
|
||||
### Паттерн использования
|
||||
|
||||
```typescript
|
||||
function Component() {
|
||||
const { user, isLoading, isAuthenticated } = useAuthContext()
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner />
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <LoginPrompt />
|
||||
}
|
||||
|
||||
return <AuthenticatedContent user={user} />
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 МИГРАЦИЯ useAuth → AuthContext
|
||||
|
||||
**✅ ЗАВЕРШЕНА:** 19 сентября 2025
|
||||
|
||||
### Что изменилось
|
||||
|
||||
| Аспект | useAuth (старое) | AuthContext (новое) |
|
||||
|--------|------------------|---------------------|
|
||||
| **Архитектура** | Изолированные хуки | Централизованный контекст |
|
||||
| **Состояние** | Дублируется в каждом компоненте | Единое состояние для всего приложения |
|
||||
| **Race conditions** | Присутствовали | Исправлены |
|
||||
| **SSR** | Проблемы с Next.js | Полная совместимость |
|
||||
| **Импорт** | `useAuth()` | `useAuthContext()` |
|
||||
|
||||
### Статистика миграции
|
||||
|
||||
- **64 файла** мигрированы
|
||||
- **0 остатков** старого кода
|
||||
- **9 backup файлов** удалены после тестирования
|
||||
- **Все тесты** пройдены успешно
|
||||
|
||||
---
|
||||
|
||||
## 🎯 ЛУЧШИЕ ПРАКТИКИ
|
||||
|
||||
### ✅ Правильные паттерны
|
||||
|
||||
```typescript
|
||||
// Корректная проверка аутентификации
|
||||
const { user, isAuthenticated, isLoading } = useAuthContext()
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingState />
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <UnauthenticatedState />
|
||||
}
|
||||
|
||||
// Теперь user гарантированно не null
|
||||
return <AuthenticatedContent user={user} />
|
||||
```
|
||||
|
||||
### ❌ Избегайте
|
||||
|
||||
```typescript
|
||||
// Неправильно - не проверяем isLoading
|
||||
const { user } = useAuthContext()
|
||||
if (user) { // может быть false positive во время загрузки
|
||||
// ...
|
||||
}
|
||||
|
||||
// Неправильно - прямое обращение к токену
|
||||
const token = getAuthToken() // используйте isAuthenticated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 БЕЗОПАСНОСТЬ
|
||||
|
||||
### Токены
|
||||
|
||||
- **Автоматическое удаление** при logout
|
||||
- **Проверка валидности** при каждом запросе
|
||||
- **Безопасное хранение** в localStorage
|
||||
|
||||
### API Keys
|
||||
|
||||
- **Шифрование** в БД
|
||||
- **Валидация** при сохранении
|
||||
- **Ротация** через UI
|
||||
|
||||
---
|
||||
|
||||
**🎉 Архитектура аутентификации SFERA готова к production использованию!**
|
@ -75,7 +75,7 @@ src/components/dashboard/
|
||||
|
||||
// Структура dashboard компонента:
|
||||
export function FulfillmentDashboard() {
|
||||
const { organization } = useAuth()
|
||||
const { user } = useAuthContext()
|
||||
|
||||
// Условная маршрутизация по функциям
|
||||
if (activeTab === 'supplies') return <FulfillmentSuppliesTab />
|
||||
|
@ -260,7 +260,7 @@ export const logistNavigation: LogistNavigationItem[] = [
|
||||
|
||||
```typescript
|
||||
export function LogistSidebar() {
|
||||
const { user, logout } = useAuth()
|
||||
const { user, logout } = useAuthContext()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { isCollapsed, toggleSidebar } = useSidebar()
|
||||
@ -324,7 +324,7 @@ export function LogistSidebar() {
|
||||
|
||||
```typescript
|
||||
export function Sidebar({ isRootInstance = false }: { isRootInstance?: boolean } = {}) {
|
||||
const { user } = useAuth()
|
||||
const { user } = useAuthContext()
|
||||
|
||||
// Защита от дубликатов
|
||||
if (typeof window !== 'undefined' && !isRootInstance && window.__SIDEBAR_ROOT_MOUNTED__) {
|
||||
|
@ -188,7 +188,7 @@ import { BaseSidebar } from './BaseSidebar'
|
||||
import { NavigationItem } from './types'
|
||||
|
||||
export function LogistSidebar() {
|
||||
const { user } = useAuth()
|
||||
const { user } = useAuthContext()
|
||||
const pathname = usePathname()
|
||||
const { isCollapsed, toggleSidebar } = useSidebar()
|
||||
|
||||
@ -284,7 +284,7 @@ import { WholesaleSidebar } from './WholesaleSidebar'
|
||||
import { LogistSidebar } from './LogistSidebar'
|
||||
|
||||
export function Sidebar() {
|
||||
const { user } = useAuth()
|
||||
const { user } = useAuthContext()
|
||||
|
||||
if (!user?.organization?.type) {
|
||||
return (
|
||||
|
Reference in New Issue
Block a user