# Directual + NextJS

> **AI-Ready Documentation**: Универсальный шаблон для создания полноценного Next.js приложения с интеграцией Directual (авторизация, WebSocket, API endpoints)

### Template

Use it: <https://github.com/directual/directual-nextjs-ru>

### 📋 Обзор

Этот шаблон описывает архитектуру современного веб-приложения на **Next.js 16+ с App Router**, интегрированного с бэкендом **Directual**. Включает безопасную авторизацию через HTTP-only cookies, WebSocket для real-time обновлений, глобальное управление состоянием и адаптивную тему.

**Стек технологий:**

* Next.js 16+ (App Router)
* TypeScript
* Tailwind CSS
* shadcn/ui (компоненты)
* Directual (бэкенд: auth, API, WebSocket)
* socket.io-client (WebSocket)

***

### 🏗️ Архитектурные принципы

#### 1. Безопасность сессии

* **HTTP-only cookie** для хранения `sessionID` (защита от XSS атак)
* SessionID НЕ хранится в localStorage или клиентском state
* API Routes в Next.js служат прокси для авторизации
* Периодическая проверка сессии через `/api/auth/check`

#### 2. Разделение клиентов Directual

* **Клиентский**: через Next.js rewrites (`apiHost: '/'`) для обхода CORS
* **Серверный**: прямое подключение к `api.directual.com` для API Routes

#### 3. Глобальное управление состоянием

Иерархия провайдеров (от внешнего к внутреннему):

```
ThemeProvider → AuthProvider → DataProvider → App
```

#### 4. WebSocket интеграция

* Singleton pattern для socket.io-client
* Подключение после получения sessionID из HTTP-only cookie
* Глобальный слушатель событий для real-time обновлений
* **Важно:** только прием событий, отправка не поддерживается Directual

***

### 📁 Структура проекта

```
project-root/
├── app/
│   ├── api/
│   │   └── auth/
│   │       ├── login/route.ts       # POST: авторизация + установка cookie
│   │       ├── register/route.ts    # POST: регистрация + установка cookie
│   │       ├── logout/route.ts      # POST: удаление cookie
│   │       ├── check/route.ts       # GET: проверка сессии
│   │       └── session/route.ts     # GET: получение sessionID для WebSocket
│   ├── layout.tsx                   # Root layout с провайдерами
│   ├── globals.css                  # CSS переменные для темы
│   └── page.tsx                     # Home page
│
├── components/
│   ├── ui/                          # shadcn/ui компоненты
│   ├── global-alerts.tsx            # Глобальные уведомления (из WebSocket)
│   └── socket-listener.tsx          # Глобальный слушатель WebSocket
│
├── context/
│   ├── auth-provider.tsx            # Контекст авторизации
│   ├── data-provider.tsx            # Контекст данных (глобальный кеш)
│   └── theme-provider.tsx           # Контекст темы (light/dark/system)
│
├── hooks/
│   ├── use-auth.ts                  # Хук для авторизации (реэкспорт)
│   ├── use-data.ts                  # Хук для данных (реэкспорт)
│   └── use-socket.ts                # Хуки для WebSocket
│
├── lib/
│   └── directual/
│       ├── client.ts                # Клиентский Directual API
│       ├── server-client.ts         # Серверный Directual API
│       ├── auth.ts                  # Функции авторизации
│       ├── fetcher.ts               # Универсальный fetcher
│       └── socket.ts                # WebSocket singleton
│
├── types/
│   ├── directual.d.ts               # Типы для directual-api
│   └── index.ts                     # Общие типы приложения
│
├── .env.local                       # Переменные окружения
├── next.config.mjs                  # Конфигурация Next.js (rewrites, headers)
├── tailwind.config.js               # Конфигурация Tailwind
└── package.json
```

***

### 🔐 Безопасная авторизация

#### Концепция

SessionID хранится в **HTTP-only cookie**, недоступной для JavaScript на клиенте. Все операции с сессией проходят через API Routes.

#### API Routes

**1. `/api/auth/login` (POST)**

* Принимает: `{ email, password }`
* Вызывает: `serverApi.auth.login()`
* Возвращает: `{ success, user }`
* Устанавливает cookie: `1cable_session` (httpOnly, secure, sameSite: strict, maxAge: 7 дней)

**2. `/api/auth/register` (POST)**

* Принимает: `{ email, password, username }`
* Вызывает: `serverApi.auth.register()`
* Возвращает: `{ success, user }`
* Устанавливает cookie: аналогично login

**3. `/api/auth/logout` (POST)**

* Читает cookie `1cable_session`
* Вызывает: `serverApi.auth.logout(sessionID)`
* Удаляет cookie (maxAge: 0)
* Возвращает: `{ success }`

**4. `/api/auth/check` (GET)**

* Читает cookie `1cable_session`
* Вызывает: `serverApi.auth.check(sessionID)`
* Возвращает: `{ success, user }` или 401
* При ошибке удаляет cookie

**5. `/api/auth/session` (GET)**

* Читает cookie `1cable_session`
* Возвращает: `{ success, sessionID }`
* Используется WebSocket для авторизации

#### AuthProvider (context/auth-provider.tsx)

**State:**

* `user: User | null` - текущий пользователь
* `loading: boolean` - загрузка
* `error: string | null` - ошибка

**Методы:**

* `login(email, password)` - вход через `/api/auth/login`
* `register(email, password, username)` - регистрация через `/api/auth/register`
* `logout()` - выход через `/api/auth/logout`
* `isAuthorized()` - проверка авторизации
* `hasRole(role)` - проверка роли (RBAC)

**Lifecycle:**

* При монтировании вызывает `/api/auth/check` для проверки сессии

***

### 🔌 Directual клиенты

#### lib/directual/client.ts (клиентский)

```typescript
import Directual from 'directual-api';

export const APP_ID = process.env.NEXT_PUBLIC_DIRECTUAL_APP_ID;

const api = new Directual({ 
  apiHost: '/',  // Используем rewrites
  appID: APP_ID
});

export default api;
```

#### lib/directual/server-client.ts (серверный)

```typescript
import Directual from 'directual-api';

export const APP_ID = process.env.NEXT_PUBLIC_DIRECTUAL_APP_ID;

const serverApi = new Directual({ 
  apiHost: 'https://api.directual.com',  // Прямое подключение
  appID: APP_ID
});

export default serverApi;
```

#### next.config.mjs (rewrites)

```javascript
async rewrites() {
  return [
    {
      source: '/good/:path*',
      destination: 'https://api.directual.com/good/:path*',
    },
  ];
}
```

**Почему два клиента?**

* Клиентский использует rewrites для обхода CORS
* Серверный используется в API Routes, где CORS не проблема

***

### 📡 Универсальный Fetcher

#### lib/directual/fetcher.ts

**Класс Fetcher** - обертка над directual-api с дополнительными фичами:

**Ключевые возможности:**

1. **Кеширование sessionID** - избегает множественных вызовов `/api/auth/session`
2. **Обработка ошибок** - унифицированная обработка ошибок API
3. **Загрузка файлов** - с отслеживанием прогресса
4. **Универсальные методы** - `get()`, `post()`, специфичные методы для структур

**Методы:**

```typescript
class Fetcher {
  // Получить sessionID из cookie (с кешированием)
  async getSessionID(): Promise<string | null>
  
  // Очистить кеш sessionID
  clearSessionCache(): void
  
  // Универсальный GET
  async get<T>(structure: string, endpoint: string, params?): Promise<GetResponse<T>>
  
  // Универсальный POST
  async post<T>(structure: string, endpoint: string, payload?, params?): Promise<PostResponse<T>>
  
  // Загрузка файлов с прогрессом
  async uploadFile(file: File, onProgress?: (percent) => void): Promise<PostResponse<{urlLink: string}>>
  
  // Специфичные методы для вашего приложения (примеры):
  async checkSession(params?): Promise<GetResponse>
  async readProfile(params?): Promise<GetResponse<UserProfile>>
  async updateProfile(payload, params?): Promise<PostResponse>
  
  // Добавьте методы для ваших структур данных:
  // async getYourEntities(params?): Promise<GetResponse<YourEntity>>
  // async createYourEntity(payload, params?): Promise<PostResponse>
}

export const fetcher = new Fetcher();
```

**Обработка ошибок:**

```typescript
// При ошибке API возвращаем структурированный объект
catch (error: unknown) {
  const err = error as { response?: { data?: { msg?: string } }; message?: string };
  return {
    success: false,
    error: err.response?.data?.msg || err.message || 'Ошибка запроса',
  };
}
```

**Важно:** 403 ошибки могут означать не только истечение сессии, но и ограничения по ролям (RBAC). Не рекомендуется автоматически разлогинивать пользователя при 403 - лучше показывать сообщение о недостаточных правах.

***

### 🌐 WebSocket интеграция

#### lib/directual/socket.ts

**Singleton pattern** для socket.io-client:

```typescript
class DirectualSocket {
  private socket: Socket;
  private isConnectedFlag: boolean;

  constructor() {
    this.socket = io('https://api.directual.com', { autoConnect: false });
  }

  // Получить sessionID из cookie через API
  async getSessionID(): Promise<string | null>

  // Подключиться (async - ждет sessionID)
  async connect(): Promise<boolean>

  // Отключиться
  disconnect(): void

  // Подписаться на событие
  on(eventName: string, callback: (...args) => void): void

  // Отписаться от события
  off(eventName: string, callback?: (...args) => void): void

  // Подписаться на ВСЕ события (дебаг)
  onAny(callback: (eventName, args) => void): void

  // Статус подключения
  getStatus(): { isConnected: boolean; socketId: string | null }
  get isConnected(): boolean
}

export const getSocket = (): DirectualSocket => {
  if (!socketInstance) {
    socketInstance = new DirectualSocket();
  }
  return socketInstance;
};
```

**Авторизация WebSocket:**

```typescript
this.socket.auth = {
  app_id: APP_ID,
  session_id: sessionID,  // Получен из /api/auth/session
};
this.socket.connect();
```

**Важно:** Directual WebSocket работает **только на прием** событий. Вы можете подписываться на события и получать их, но отправка событий в Directual НЕ поддерживается. Все изменения данных делайте через HTTP API (Fetcher).

#### hooks/use-socket.ts

**Хуки для работы с WebSocket в React:**

```typescript
// Получить socket инстанс
export function useSocket(): { socket, isConnected }

// Подписаться на конкретное событие (основной хук)
export function useSocketEvent(eventName: string, callback: (...args) => void)
```

**Примечание:** Хук `useSocketEmit` НЕ нужен, так как Directual WebSocket не принимает события от клиента. Все изменения данных делайте через HTTP API.

#### components/socket-listener.tsx

**Глобальный слушатель** для всего приложения:

* Подключается к WebSocket при монтировании
* Логирует все события в консоль
* Обрабатывает специальные события (например, `alert`)
* Рендерит `null` (невидимый компонент)

**Пример обработки события `alert`:**

```typescript
if (eventName === 'alert') {
  let alertData = args[0];
  if (Array.isArray(alertData)) alertData = alertData[0];
  if (typeof alertData === 'string') alertData = JSON.parse(alertData);
  
  if (window.__showGlobalAlert) {
    window.__showGlobalAlert(alertData);
  }
}
```

***

### 📦 Глобальное управление данными

#### context/data-provider.tsx

**DataProvider** - глобальный кеш данных приложения с автоматическим обновлением.

**Концепция:** Храним в контексте те данные, которые нужны во многих местах приложения (профиль пользователя, настройки, списки сущностей и т.д.). Это избавляет от повторных запросов и обеспечивает консистентность данных.

**Структура State:**

* `userProfile: UserProfile | null` - профиль текущего пользователя
* `settings: Record<string, unknown> | null` - настройки приложения
* `[yourEntityList]: YourEntity[]` - списки ваших сущностей (адаптируйте под проект)
* `loading: boolean` - первоначальная загрузка
* `refreshing: boolean` - фоновое обновление данных
* `error: string | null` - ошибка загрузки

**Методы:**

* `refreshProfile(silent?)` - обновить профиль пользователя
* `refreshSettings(silent?)` - обновить настройки
* `refreshYourEntity(silent?)` - обновить вашу сущность (адаптируйте)
* `refreshAll(silent?)` - обновить все данные
* `updateProfile(profileData)` - обновить профиль

**Автоматическое обновление:**

1. При монтировании (если `isAuthorized()`)
2. При WebSocket событиях: `refresh`, `entity_created`, `entity_updated`, `entity_deleted`
3. При возврате на вкладку браузера (`visibilitychange`)

**Silent refresh:**

* `silent=true` - обновление без `loading` индикатора
* Используется для фоновых обновлений (WebSocket, visibilitychange)

**Пример использования:**

```typescript
const { userProfile, settings, loading, refreshAll } = useData();
```

**Адаптация под проект:** Замените примеры методов на ваши сущности. Принцип остается тот же - централизованное хранилище данных с автоматической синхронизацией через WebSocket.

***

### 📤 Загрузка файлов

#### Настройка структуры в Directual

**Обязательный шаг:** Создайте структуру данных `file_links` в Directual:

1. Создайте структуру с именем `file_links`
2. Добавьте поле `link` с типом **FileUpload**
3. Настройте права доступа:
   * **Write**: разрешите запись для авторизованных пользователей
   * **Read**: настройте по необходимости
4. Создайте эндпоинт `uploadFiles` (POST) для загрузки файлов

**Структура поля:**

```
Название структуры: file_links
Поле: link
Тип: FileUpload
Доступ на запись: ✅ Enabled
```

#### Метод uploadFile в Fetcher

Fetcher включает специальный метод для загрузки файлов на Directual с отслеживанием прогресса.

**Сигнатура:**

```typescript
async uploadFile(
  file: File, 
  onProgress?: (percent: number) => void
): Promise<PostResponse<{ urlLink: string }>>
```

**Механизм работы:**

1. **FormData** - файл оборачивается в FormData для multipart/form-data
2. **Прокси через rewrites** - используется `/good/api/v5/data/file_links/uploadFiles`
3. **SessionID из cookie** - автоматически добавляется в query параметры
4. **XMLHttpRequest** - для отслеживания прогресса (если передан `onProgress`)
5. **Fallback на fetch** - если `onProgress` не нужен

**Структура запроса:**

```
POST /good/api/v5/data/file_links/uploadFiles?appID={APP_ID}&sessionID={sessionID}
Content-Type: multipart/form-data
Body: FormData с файлом
```

**Пример использования:**

```typescript
const handleFileUpload = async (file: File) => {
  const [progress, setProgress] = useState(0);
  
  const result = await fetcher.uploadFile(file, (percent) => {
    setProgress(percent); // Обновляем прогресс-бар
  });
  
  if (result.success && result.data) {
    const fileUrl = result.data.urlLink; // URL загруженного файла
    console.log('Файл загружен:', fileUrl);
  } else {
    console.error('Ошибка загрузки:', result.error);
  }
};
```

**Компонент с прогресс-баром:**

```typescript
import { useState } from 'react';
import { Progress } from '@/components/ui/progress';
import { Button } from '@/components/ui/button';
import { fetcher } from '@/lib/directual/fetcher';

function FileUploader() {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);
    setProgress(0);

    const result = await fetcher.uploadFile(file, (percent) => {
      setProgress(Math.round(percent));
    });

    if (result.success && result.data) {
      console.log('URL файла:', result.data.urlLink);
      // Сохраните URL в вашу структуру данных
    }

    setUploading(false);
  };

  return (
    <div>
      <input 
        type="file" 
        onChange={handleChange} 
        disabled={uploading}
      />
      {uploading && (
        <div>
          <Progress value={progress} />
          <p>{progress}%</p>
        </div>
      )}
    </div>
  );
}
```

**Важные детали:**

1. **credentials: 'include'** - обязательно для отправки cookie с sessionID
2. **XMLHttpRequest.upload.progress** - используется для отслеживания прогресса
3. **Обработка 403** - если сессия невалидна, вернется ошибка
4. **Возвращаемый объект** - `{ urlLink: string }` - прямая ссылка на файл в Directual

**Структура ответа от Directual:**

```json
{
  "result": {
    "urlLink": "https://api.directual.com/fileUploaded/...",
    "fileName": "example.jpg",
    "fileSize": 102400,
    "fileType": "image/jpeg"
  }
}
```

**Рекомендации:**

* ⚠️ **Обязательно создайте структуру `file_links` с полем `link` (FileUpload) в Directual**
* Проверяйте размер файла на клиенте перед загрузкой
* Показывайте прогресс для файлов > 1MB
* Добавьте валидацию типов файлов (MIME types)
* Обрабатывайте ошибки сети (abort, timeout)
* Настройте права доступа на запись для структуры `file_links`

***

### 🎯 Паттерн работы с Directual

#### Рекомендуемый подход: Single Action Endpoint

**Концепция:** Все действия пользователя с фронтенда отправляются в **один универсальный эндпоинт** (например, `user_actions`). Directual сценарий анализирует тип действия и возвращает результат либо синхронно, либо через WebSocket.

**Преимущества:**

* ✅ Единая точка входа для всех действий
* ✅ Легко логировать и анализировать действия пользователей
* ✅ Гибкость в обработке (синхронный или асинхронный ответ)
* ✅ Простота добавления новых действий без изменения API

#### Структура запроса

```typescript
interface UserAction {
  action: string;              // Тип действия: 'create_entity', 'update_settings', etc.
  payload?: Record<string, unknown>;  // Данные действия
  [key: string]: unknown;      // Дополнительные параметры
}
```

#### Пример использования

```typescript
// В Fetcher добавляем метод
async postAction(payload: UserAction, params = {}): Promise<PostResponse> {
  return this.post('user_actions', 'postUserAction', payload, params);
}

// Использование в компоненте
const createEntity = async (entityData) => {
  const result = await fetcher.postAction({
    action: 'create_entity',
    payload: entityData
  });
  
  if (result.success) {
    // Либо результат пришел синхронно в result.data
    // Либо придет через WebSocket событие 'entity_created'
  }
};
```

#### Обработка в Directual

**Сценарий в эндпоинте `postUserAction`:**

1. Читаем `action` из запроса
2. Switch/case по типу действия
3. Выполняем бизнес-логику
4. Возвращаем результат:
   * **Синхронно**: return result (быстрые операции)
   * **Асинхронно**: отправляем WebSocket событие (долгие операции)

**Пример структуры:**

```
postUserAction:
  ├─ IF action = 'create_entity'
  │   ├─ Создать запись
  │   ├─ Отправить WebSocket: 'entity_created'
  │   └─ Return { success: true }
  │
  ├─ IF action = 'update_settings'
  │   ├─ Обновить настройки
  │   └─ Return { success: true, data: newSettings }
  │
  └─ ELSE
      └─ Return { success: false, error: 'Unknown action' }
```

#### WebSocket события для ответов

Для асинхронных операций отправляйте WebSocket события:

* `entity_created` → DataProvider обновляет список
* `entity_updated` → DataProvider обновляет список
* `entity_deleted` → DataProvider обновляет список
* `alert` → GlobalAlerts показывает уведомление
* `progress` → Индикатор прогресса (для долгих операций)

#### Когда использовать синхронный vs асинхронный ответ

**Синхронный (return result):**

* Быстрые операции (< 1 сек)
* Когда нужен немедленный ответ для UI
* Операции чтения (GET)
* Обновление профиля, настроек

**Асинхронный (WebSocket):**

* Долгие операции (> 1 сек)
* Когда результат нужен не сразу
* Операции, влияющие на других пользователей
* Создание/удаление сущностей
* Фоновые задачи

***

### 🎨 Система темизации

#### context/theme-provider.tsx

**ThemeProvider** - управление темой приложения.

**State:**

* `theme: 'light' | 'dark' | 'system'` - выбранная тема
* `actualTheme: 'light' | 'dark'` - реальная применяемая тема
* `mounted: boolean` - флаг монтирования

**Методы:**

* `setTheme(theme)` - установить тему
* `toggleTheme()` - переключить light ↔ dark
* `isDark`, `isLight`, `isSystem` - утилиты проверки

**Механизм работы:**

1. Тема хранится в `localStorage.theme`
2. Применяется через класс `light` или `dark` на `<html>`
3. При `system` слушает `prefers-color-scheme` media query
4. При изменении системной темы автоматически переключается

**CSS переменные (app/globals.css):**

```css
:root {
  --background: #ffffff;
  --foreground: #000000;
  --primary: #...;
  /* ... */
}

.dark {
  --background: #000000;
  --foreground: #ffffff;
  --primary: #...;
  /* ... */
}
```

#### tailwind.config.js

```javascript
module.exports = {
  darkMode: ["class"],  // Используем class-based режим
  theme: {
    extend: {
      colors: {
        background: "var(--background)",
        foreground: "var(--foreground)",
        primary: {
          DEFAULT: "var(--primary)",
          foreground: "var(--primary-foreground)",
        },
        // ...
      },
    },
  },
};
```

***

### 🔒 Security Headers

#### next.config.mjs

**Обязательные security headers:**

```javascript
async headers() {
  return [
    {
      source: '/:path*',
      headers: [
        {
          key: 'X-Frame-Options',
          value: 'DENY',  // Защита от clickjacking
        },
        {
          key: 'X-Content-Type-Options',
          value: 'nosniff',  // MIME sniffing защита
        },
        {
          key: 'X-XSS-Protection',
          value: '1; mode=block',  // XSS фильтр браузера
        },
        {
          key: 'Referrer-Policy',
          value: 'strict-origin-when-cross-origin',
        },
        {
          key: 'Permissions-Policy',
          value: 'camera=(), microphone=(), geolocation=()',
        },
        {
          key: 'Content-Security-Policy',
          value: [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "connect-src 'self' https://api.directual.com wss://api.directual.com",
            "frame-ancestors 'none'",
          ].join('; '),
        },
      ],
    },
  ];
}
```

**Важно:**

* `unsafe-eval` может потребоваться для CodeMirror или других редакторов
* `connect-src` должен включать Directual API и WebSocket
* `frame-ancestors 'none'` запрещает iframe embedding

***

### 📝 Типизация TypeScript

#### types/index.ts

**Базовые типы:**

```typescript
// API Response
export interface ApiResponse<T = unknown> {
  success: boolean;
  data?: T;
  error?: string;
  pageInfo?: PageInfo | null;
  status?: string | null;
}

export interface PageInfo {
  page: number;
  pageSize: number;
  totalPages: number;
}

// User
export interface User {
  id: string;
  email: string;
  username: string;
  role: string;
  avatar: string | null;
}

export interface UserProfile {
  id: string;
  firstName: string;
  lastName: string;
  userpic: string;
  email?: string;
  username?: string;
}

// Auth Context
export interface AuthContextValue {
  user: User | null;
  sessionID: string | null;  // Всегда null на клиенте
  loading: boolean;
  error: string | null;
  login: (email: string, password: string) => Promise<ApiResponse<User>>;
  register: (email: string, password: string, username: string) => Promise<ApiResponse<User>>;
  logout: () => Promise<void>;
  isAuthorized: () => boolean;
  hasRole: (role: string) => boolean;
}

// Data Context (адаптируйте под свои сущности)
export interface DataContextValue {
  userProfile: UserProfile | null;
  settings: Record<string, unknown> | null;
  // Добавьте ваши сущности:
  // entities: YourEntity[];
  loading: boolean;
  refreshing: boolean;
  error: string | null;
  refreshProfile: (silent?: boolean) => Promise<void>;
  refreshSettings: (silent?: boolean) => Promise<void>;
  // Добавьте методы для ваших сущностей:
  // refreshEntities: (silent?: boolean) => Promise<void>;
  refreshAll: (silent?: boolean) => Promise<void>;
  updateProfile: (profileData: Partial<UserProfile>) => Promise<ApiResponse<void>>;
}

// Theme
export type Theme = 'light' | 'dark' | 'system';

// WebSocket
export type SocketEventCallback = (...args: unknown[]) => void;

export interface UseSocketResult {
  connected: boolean;
  error: string | null;
  emit: (event: string, data?: unknown) => void;
}
```

#### types/directual.d.ts

**Расширение типов для directual-api:**

```typescript
declare module 'directual-api' {
  export default class Directual {
    constructor(config: { apiHost: string; appID: string });
    
    auth: {
      login(email: string, password: string): Promise<Token>;
      register(email: string, password: string, data: any): Promise<Token>;
      logout(sessionID: string): Promise<void>;
      check(sessionID: string): Promise<Token>;
    };
    
    structure(name: string): {
      getData(endpoint: string, params?: any): Promise<{ payload: any[]; pageInfo?: any }>;
      setData(endpoint: string, payload: any, params?: any): Promise<any>;
    };
  }
  
  export interface Token {
    sessionID: string;
    username: string;
    role: string;
    nid?: string;
  }
}
```

***

### 🚀 Root Layout

#### app/layout.tsx

**Структура провайдеров:**

```typescript
export default function RootLayout({ children }) {
  return (
    <html lang="ru" suppressHydrationWarning>
      <body>
        <ThemeProvider>
          <AuthProvider>
            <DataProvider>
              {/* Глобальный WebSocket слушатель */}
              <SocketListener />
              
              {/* Глобальные алерты из WebSocket */}
              <GlobalAlerts />
              
              {children}
            </DataProvider>
          </AuthProvider>
        </ThemeProvider>
      </body>
    </html>
  );
}
```

**Важно:**

* `suppressHydrationWarning` - для темы (класс меняется до hydration)
* Порядок провайдеров: Theme → Auth → Data
* SocketListener и GlobalAlerts монтируются глобально

***

### 🎯 Глобальные алерты

#### components/global-alerts.tsx

**Механизм:**

1. WebSocket отправляет событие `alert` с данными
2. SocketListener перехватывает и вызывает `window.__showGlobalAlert()`
3. GlobalAlerts отображает toast-уведомление

**Структура алерта:**

```typescript
interface AlertData {
  type?: 'success' | 'error' | 'default';
  variant?: 'success' | 'destructive' | 'default';
  title?: string;
  description?: string;  // Может содержать HTML
  icon?: string;  // Имя иконки из lucide-react
}
```

**Фичи:**

* Автоматическое скрытие через 5 секунд
* Дедупликация (один и тот же алерт не показывается дважды)
* Ручное закрытие (кнопка X)
* Поддержка HTML в description
* Динамические иконки из lucide-react

***

### ⚙️ Environment Variables

#### .env.local

```bash
# Directual App ID (обязательная переменная)
NEXT_PUBLIC_DIRECTUAL_APP_ID=your_app_id_here

# NODE_ENV автоматически устанавливается Next.js
# development | production | test
```

**Важно:**

* Префикс `NEXT_PUBLIC_` делает переменную доступной на клиенте
* Без этой переменной приложение не запустится (проверка в `lib/directual/client.ts`)

***

### 📦 Dependencies

#### package.json (основные зависимости)

```json
{
  "dependencies": {
    "next": "^16.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    
    "directual-api": "^1.4.0",
    "socket.io-client": "^4.8.0",
    
    "tailwindcss": "^3.4.0",
    "tailwindcss-animate": "^1.0.7",
    "clsx": "^2.1.0",
    "tailwind-merge": "^3.4.0",
    "class-variance-authority": "^0.7.0",
    
    "@radix-ui/react-dialog": "^1.1.0",
    "@radix-ui/react-popover": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.0",
    "@radix-ui/react-tooltip": "^1.2.0",
    "@radix-ui/react-avatar": "^1.1.0",
    "@radix-ui/react-alert-dialog": "^1.1.0",
    
    "lucide-react": "^0.550.0"
  },
  "devDependencies": {
    "@types/node": "^24.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "typescript": "^5.9.0",
    "autoprefixer": "^10.4.0",
    "postcss": "^8.5.0",
    "eslint": "^9.0.0",
    "eslint-config-next": "^16.0.0"
  }
}
```

#### scripts

```json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  }
}
```

***

### 🔄 Жизненный цикл приложения

#### 1. Инициализация (app load)

1. Root Layout монтируется
2. **ThemeProvider** загружает тему из localStorage и применяет
3. **AuthProvider** проверяет сессию через `/api/auth/check`
4. **DataProvider** загружает данные (если авторизован)
5. **SocketListener** подключается к WebSocket (если авторизован)

#### 2. Авторизация (login flow)

1. Пользователь вводит credentials
2. `authContext.login()` отправляет POST `/api/auth/login`
3. API route вызывает `serverApi.auth.login()`
4. При успехе устанавливается HTTP-only cookie
5. AuthProvider обновляет `user` state
6. DataProvider автоматически загружает данные
7. SocketListener подключается к WebSocket

#### 3. Работа с данными

1. Компонент вызывает `refreshYourEntity()` или `refreshAll()`
2. Fetcher получает `sessionID` из `/api/auth/session` (с кешем)
3. Выполняется запрос к Directual через `api.structure().getData()`
4. DataProvider обновляет state
5. React перерендеривает компоненты

#### 4. WebSocket обновления

1. Сервер отправляет событие (например, `entity_created`)
2. SocketListener перехватывает событие
3. DataProvider подписан на это событие через `useSocketEvent()`
4. Вызывается `refreshYourEntity(true)` (silent)
5. UI обновляется автоматически

#### 5. Обработка ошибок доступа

1. Fetcher получает 403 ошибку
2. Возвращается `{ success: false, error: 'Недостаточно прав' }`
3. Компонент показывает сообщение об ошибке
4. Пользователь остается авторизованным (403 ≠ протухшая сессия)

**Примечание:** 403 может означать RBAC ограничения, а не истечение сессии. Для проверки сессии используйте периодический вызов `/api/auth/check`.

#### 6. Выход (logout flow)

1. Пользователь нажимает "Выйти"
2. `authContext.logout()` отправляет POST `/api/auth/logout`
3. API route удаляет cookie
4. AuthProvider очищает `user` state
5. Fetcher очищает кеш sessionID
6. SocketListener отключается от WebSocket
7. Редирект на страницу входа

***

### 🎓 Best Practices

#### 1. Безопасность

* ✅ Всегда используй HTTP-only cookie для sessionID
* ✅ Никогда не храни sessionID в localStorage
* ✅ Используй серверный клиент в API Routes
* ✅ Добавь security headers в next.config.mjs
* ✅ Проверяй сессию при каждом критичном запросе

#### 2. Performance

* ✅ Используй `silent=true` для фоновых обновлений
* ✅ Кешируй sessionID в Fetcher
* ✅ WebSocket singleton предотвращает множественные подключения
* ✅ React Context оптимизирован (useCallback с \[])

#### 3. UX

* ✅ Показывай loading только при первоначальной загрузке
* ✅ Используй `refreshing` для индикатора обновления
* ✅ Автоматически обновляй данные при возврате на вкладку
* ✅ Показывай понятные ошибки пользователю

#### 4. Архитектура

* ✅ Разделяй клиентский и серверный код
* ✅ Используй TypeScript для type safety
* ✅ Следуй иерархии провайдеров
* ✅ Держи бизнес-логику в контекстах, не в компонентах

#### 5. Directual

* ✅ Всегда передавай `sessionID` в запросах
* ✅ Обрабатывай 403 ошибки глобально
* ✅ Используй структуры данных правильно
* ✅ WebSocket требует `app_id` + `session_id` в `socket.auth`

***

### 🐛 Troubleshooting

#### Проблема: WebSocket не подключается

**Причины:**

* sessionID не найден (не авторизован)
* Неверный APP\_ID
* Неверный формат `socket.auth`

**Решение:**

* Проверь что cookie установлена (`document.cookie`)
* Проверь что `/api/auth/session` возвращает sessionID
* Убедись что `NEXT_PUBLIC_DIRECTUAL_APP_ID` установлен

#### Проблема: 403 ошибка на запросе

**Причины:**

* Недостаточно прав (RBAC ограничения)
* Неверная роль пользователя для этого эндпоинта
* sessionID не передается в запросах
* Cookie не отправляется (credentials: 'include')

**Решение:**

* Проверь права пользователя в Directual (структура WebUser, поле role)
* Проверь что `fetcher.getSessionID()` возвращает sessionID
* Убедись что `credentials: 'include'` в fetch
* Проверь настройки эндпоинта в Directual (Access control)

#### Проблема: Данные не обновляются

**Причины:**

* DataProvider не подписан на WebSocket события
* WebSocket не подключен
* Неверное имя события

**Решение:**

* Проверь что SocketListener монтирован в layout
* Проверь консоль на наличие логов `[WebSocket] eventName`
* Убедись что структура данных в событии правильная

#### Проблема: Тема не применяется

**Причины:**

* CSS переменные не определены
* Класс `dark` не добавляется на `<html>`
* Tailwind не настроен на `darkMode: ["class"]`

**Решение:**

* Проверь `app/globals.css` на наличие `:root` и `.dark`
* Проверь что ThemeProvider монтирован
* Проверь `tailwind.config.js`

***

### 📚 Дополнительные ресурсы

#### Directual

* [Directual API Documentation](https://docs.directual.com)
* [directual-api NPM Package](https://www.npmjs.com/package/directual-api)

#### Next.js

* [Next.js Documentation](https://nextjs.org/docs)
* [App Router Guide](https://nextjs.org/docs/app)
* [API Routes](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)

#### Security

* [OWASP Security Headers](https://owasp.org/www-project-secure-headers/)
* [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)

#### React Patterns

* [Context API](https://react.dev/learn/passing-data-deeply-with-context)
* [Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks)

***

### 🚪 Страница входа (Login Page)

#### Что нужно реализовать

Шаблон НЕ включает готовую страницу входа "из коробки". Вам нужно создать:

**1. Страницу `/app/auth/login/page.tsx`:**

```typescript
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/use-auth';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

export default function LoginPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { login, loading, error } = useAuth();
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await login(email, password);
    if (result.success) {
      router.push('/dashboard'); // Редирект после входа
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <Button type="submit" disabled={loading}>Войти</Button>
      {error && <p>{error}</p>}
    </form>
  );
}
```

**2. Страницу `/app/auth/register/page.tsx`** (аналогично)

**3. Middleware для защиты роутов (опционально):**

```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const sessionCookie = request.cookies.get('1cable_session');
  
  // Если нет сессии и пытается зайти на защищенную страницу
  if (!sessionCookie && !request.nextUrl.pathname.startsWith('/auth')) {
    return NextResponse.redirect(new URL('/auth/login', request.url));
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
};
```

***

### 🧩 shadcn/ui Компоненты

#### Что входит "из коробки"

Шаблон предполагает использование **shadcn/ui**, но НЕ устанавливает компоненты автоматически.

**Вам нужно установить вручную:**

```bash
# Инициализация shadcn/ui
npx shadcn@latest init

# Базовые компоненты для авторизации
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add card
npx shadcn@latest add form

# Для алертов и уведомлений
npx shadcn@latest add alert
npx shadcn@latest add alert-dialog
npx shadcn@latest add toast
npx shadcn@latest add sonner  # Альтернатива тостам

# Для UI приложения
npx shadcn@latest add dialog
npx shadcn@latest add popover
npx shadcn@latest add dropdown-menu
npx shadcn@latest add tabs
npx shadcn@latest add tooltip
npx shadcn@latest add avatar
npx shadcn@latest add skeleton  # Для loading states
```

#### GlobalAlerts vs Toast

В шаблоне есть **GlobalAlerts** (компонент для WebSocket алертов), который работает аналогично тостам, но:

* Показывает алерты из WebSocket
* Позиционируется `fixed top-4 right-4`
* Автоматически скрывается через 5 секунд

Если вам нужны **обычные тосты** для клиентских уведомлений:

**Вариант 1: shadcn/ui Toast**

```bash
npx shadcn@latest add toast
```

**Вариант 2: Sonner (рекомендуется)**

```bash
npx shadcn@latest add sonner
```

Используйте в приложении:

```typescript
import { toast } from 'sonner';

toast.success('Операция выполнена');
toast.error('Произошла ошибка');
```

#### Рекомендуемый набор компонентов

**Минимальный (для авторизации):**

* button, input, card, alert

**Стандартный (для приложения):**

* dialog, popover, dropdown-menu, tabs, tooltip, avatar, skeleton, toast/sonner

**Расширенный (для сложных UI):**

* command, combobox, select, radio-group, checkbox, switch, slider, calendar, date-picker, table, pagination

***

### ✅ Чеклист для нового проекта

При создании нового приложения по этому шаблону:

**Шаг 1: Инициализация**

* [ ] `npx create-next-app@latest` с TypeScript и App Router
* [ ] Установить зависимости: `directual-api`, `socket.io-client`, `tailwindcss`, `lucide-react`
* [ ] `npx shadcn@latest init` для инициализации shadcn/ui
* [ ] Установить базовые shadcn компоненты (button, input, card, alert, toast)
* [ ] Создать `.env.local` с `NEXT_PUBLIC_DIRECTUAL_APP_ID`

**Шаг 2: Конфигурация**

* [ ] Настроить rewrites в `next.config.mjs`
* [ ] Добавить security headers в `next.config.mjs`
* [ ] Настроить CSS переменные для темы в `globals.css`
* [ ] Настроить Tailwind (`darkMode: ["class"]`)

**Шаг 3: Структура проекта**

* [ ] Создать структуру папок: `app/api/auth/`, `lib/directual/`, `context/`, `hooks/`, `types/`, `components/ui/`
* [ ] Создать типы в `types/index.ts` и `types/directual.d.ts`

**Шаг 4: Directual интеграция**

* [ ] Реализовать Directual клиенты (client.ts, server-client.ts)
* [ ] Реализовать API Routes для авторизации (login, register, logout, check, session)
* [ ] Создать Fetcher класс с обработкой сессии
* [ ] Создать WebSocket singleton
* [ ] Добавить методы в Fetcher для ваших структур данных

**Шаг 5: Провайдеры и контексты**

* [ ] Создать ThemeProvider
* [ ] Создать AuthProvider
* [ ] Создать DataProvider (адаптировать под ваши сущности)
* [ ] Создать хуки (use-auth, use-data, use-socket)
* [ ] Настроить Root Layout с провайдерами

**Шаг 6: UI компоненты**

* [ ] Добавить SocketListener и GlobalAlerts в layout
* [ ] Создать страницы `/app/auth/login/page.tsx` и `/app/auth/register/page.tsx`
* [ ] Создать middleware для защиты роутов (опционально)
* [ ] Создать компоненты для вашего приложения

**Шаг 7: Directual Backend**

* [ ] Настроить структуру `user_actions` в Directual
* [ ] Создать эндпоинт `postUserAction` с обработкой разных действий
* [ ] Настроить WebSocket события для асинхронных ответов
* [ ] Протестировать синхронные и асинхронные операции

**Шаг 8: Тестирование**

* [ ] Протестировать авторизацию (login/logout/register)
* [ ] Протестировать WebSocket подключение
* [ ] Протестировать переключение темы
* [ ] Протестировать работу с данными (CRUD операции)
* [ ] Протестировать обработку истечения сессии (403)
* [ ] Проверить security headers (browser DevTools → Network → Headers)
