# 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)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://readme.directual.com/directual-react-js/directual-+-nextjs.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
