# План рефакторинга интеграции с Todoist

## Цель
Переделать интеграцию с Todoist для использования **единого приложения**, созданного в Todoist Developer Platform. Все пользователи Play Life используют одно Todoist приложение с единым webhook URL.

## Текущая реализация
- Каждый пользователь имеет уникальный `webhook_token` в таблице `todoist_integrations`
- Webhook URL: `/webhook/todoist/{token}` (токен в URL)
- Пользователь определяется по токену из URL
- Пользователь должен вручную копировать webhook URL

## Новая реализация (Единое приложение)
- **Единое Todoist приложение** для всех пользователей Play Life
- **Единый Webhook URL:** `/webhook/todoist` (без токена!)
- Webhook настроен в Todoist Developer Console на уровне приложения
- Пользователь определяется по `todoist_user_id` из `event_data` webhook
- OAuth используется для привязки Todoist аккаунта к Play Life аккаунту
- **Пользователю не нужно ничего настраивать** — просто нажать "Подключить Todoist"!

## Краткое резюме изменений

### База данных:
- **Удалить** поле `webhook_token` (больше не нужно!)
- Добавить поля: `todoist_user_id`, `todoist_email`, `access_token`

### Переменные окружения:
- `TODOIST_CLIENT_ID` - Client ID приложения
- `TODOIST_CLIENT_SECRET` - Client Secret приложения
- `WEBHOOK_BASE_URL` - для формирования OAuth Redirect URI

### Backend:
- **Изменить webhook handler** — идентификация по `todoist_user_id`
- Добавить OAuth endpoints для подключения/отключения
- Убрать логику с токенами в URL

### Frontend:
- **Убрать отображение webhook URL** (не нужно!)
- Показать кнопку "Подключить Todoist"
- После подключения показать email и статус

---

## 1. Изменения в базе данных

### Миграция: `013_refactor_todoist_single_app.sql`

**Изменения в таблице `todoist_integrations`:**

1. **Удалить:**
   - `webhook_token` — больше не нужен! Webhook единый для всего приложения.

2. **Добавить:**
   - `todoist_user_id` (BIGINT) — ID пользователя в Todoist (из OAuth, для идентификации в webhook)
   - `todoist_email` (VARCHAR(255)) — Email пользователя в Todoist (из OAuth)
   - `access_token` (TEXT) — OAuth access token (бессрочный в Todoist)

3. **Индексы:**
   - **Уникальный** индекс на `todoist_user_id` — ключевой для идентификации в webhook!
   - Уникальный индекс на `todoist_email`
   - Удалить индекс на `webhook_token`

**Структура после миграции:**
```sql
CREATE TABLE todoist_integrations (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    todoist_user_id BIGINT,  -- ID пользователя в Todoist (КЛЮЧЕВОЕ для webhook!)
    todoist_email VARCHAR(255),  -- Email пользователя в Todoist
    access_token TEXT,  -- OAuth access token (бессрочный)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT todoist_integrations_user_id_unique UNIQUE (user_id)
);

-- Индексы
CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_todoist_user_id 
ON todoist_integrations(todoist_user_id) 
WHERE todoist_user_id IS NOT NULL;

CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_todoist_email 
ON todoist_integrations(todoist_email) 
WHERE todoist_email IS NOT NULL;
```

**Ключевое изменение:** `todoist_user_id` теперь используется для идентификации пользователя при получении webhook от Todoist.

---

## 2. Переменные окружения (.env)

### Добавить в `env.example`:

```env
# ============================================
# Todoist OAuth Configuration
# ============================================
# Client ID единого Todoist приложения
# Получить в: https://developer.todoist.com/appconsole.html
TODOIST_CLIENT_ID=your-todoist-client-id

# Client Secret единого Todoist приложения
TODOIST_CLIENT_SECRET=your-todoist-client-secret

# Секрет для проверки подлинности webhook от Todoist (опционально)
# Если задан, все запросы должны содержать заголовок X-Todoist-Webhook-Secret с этим значением
TODOIST_WEBHOOK_SECRET=
```

**Что нужно получить из Todoist приложения:**
1. `TODOIST_CLIENT_ID` - Client ID приложения
2. `TODOIST_CLIENT_SECRET` - Client Secret приложения
3. `TODOIST_WEBHOOK_SECRET` (опционально) - для дополнительной безопасности webhook

**Важно:** В настройках Todoist приложения нужно указать Redirect URI:
- Используйте: `<WEBHOOK_BASE_URL>/api/integrations/todoist/oauth/callback`
- Например, если `WEBHOOK_BASE_URL=https://your-domain.com`, то Redirect URI: `https://your-domain.com/api/integrations/todoist/oauth/callback`

---

## 3. Изменения в Backend (main.go)

### 3.1. Обновить структуру `TodoistIntegration`:
```go
type TodoistIntegration struct {
    ID            int        `json:"id"`
    UserID        int        `json:"user_id"`
    TodoistUserID *int64     `json:"todoist_user_id,omitempty"`  // Ключевое для webhook!
    TodoistEmail  *string    `json:"todoist_email,omitempty"`
    AccessToken   *string    `json:"-"`  // Не отдавать в JSON!
    CreatedAt     *time.Time `json:"created_at,omitempty"`
    UpdatedAt     *time.Time `json:"updated_at,omitempty"`
}
```

**Важно:** 
- `AccessToken` не должен отдаваться в JSON ответах (используйте `json:"-"`)
- `TodoistUserID` — ключевое поле для идентификации пользователя в webhook

### 3.2. Webhook handler (`todoistWebhookHandler`) - КЛЮЧЕВОЕ ИЗМЕНЕНИЕ:

**Новый подход:**
- URL: `/webhook/todoist` (БЕЗ токена!)
- Webhook настроен в Todoist Developer Console для всего приложения
- Извлекает `user_id` из `event_data` webhook
- Находит пользователя по `todoist_user_id`

**Новая логика:**
```go
func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
    // CORS, OPTIONS handling
    if r.Method == "OPTIONS" {
        setCORSHeaders(w)
        w.WriteHeader(http.StatusOK)
        return
    }
    setCORSHeaders(w)
    
    // Проверка webhook secret (если настроен)
    todoistWebhookSecret := getEnv("TODOIST_WEBHOOK_SECRET", "")
    if todoistWebhookSecret != "" {
        providedSecret := r.Header.Get("X-Todoist-Hmac-SHA256")
        // TODO: проверить HMAC подпись
    }
    
    // Парсим webhook
    var webhook TodoistWebhook
    if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil {
        log.Printf("Todoist webhook: error decoding: %v", err)
        w.WriteHeader(http.StatusOK)
        return
    }
    
    log.Printf("Todoist webhook: event=%s", webhook.EventName)
    
    // Обрабатываем только item:completed
    if webhook.EventName != "item:completed" {
        log.Printf("Todoist webhook: ignoring event %s", webhook.EventName)
        w.WriteHeader(http.StatusOK)
        return
    }
    
    // Извлекаем user_id из event_data (это Todoist user_id!)
    // Может приходить как string или float64
    var todoistUserID int64
    switch v := webhook.EventData["user_id"].(type) {
    case float64:
        todoistUserID = int64(v)
    case string:
        todoistUserID, _ = strconv.ParseInt(v, 10, 64)
    default:
        log.Printf("Todoist webhook: user_id not found or invalid type in event_data")
        w.WriteHeader(http.StatusOK)
        return
    }
    
    // Находим пользователя Play Life по todoist_user_id
    var userID int
    err := a.DB.QueryRow(`
        SELECT user_id FROM todoist_integrations 
        WHERE todoist_user_id = $1
    `, todoistUserID).Scan(&userID)
    
    if err == sql.ErrNoRows {
        // Пользователь не подключил Play Life — игнорируем
        log.Printf("Todoist webhook: no user found for todoist_user_id=%d (ignoring)", todoistUserID)
        w.WriteHeader(http.StatusOK)
        return
    }
    if err != nil {
        log.Printf("Todoist webhook: DB error: %v", err)
        w.WriteHeader(http.StatusOK)
        return
    }
    
    log.Printf("Todoist webhook: todoist_user_id=%d -> user_id=%d", todoistUserID, userID)
    
    // ... остальная логика обработки события (как раньше) ...
}
```

### 3.3. Маршрут webhook - ИЗМЕНИТЬ:
```go
// Было:
r.HandleFunc("/webhook/todoist/{token}", app.todoistWebhookHandler).Methods("POST", "OPTIONS")

// Стало:
r.HandleFunc("/webhook/todoist", app.todoistWebhookHandler).Methods("POST", "OPTIONS")
```

**Важно:** Этот URL нужно указать в Todoist Developer Console при настройке приложения!

### 3.4. Добавить OAuth endpoints:

1. **Инициация OAuth:**
   - `GET /api/integrations/todoist/oauth/connect` - перенаправляет на Todoist OAuth
   - **ВАЖНО:** Требует авторизацию пользователя (JWT token в cookie или header)
   - Генерирует `state` параметр с user_id (JWT подписанный jwtSecret)
   - Формирует `redirect_uri` из `WEBHOOK_BASE_URL`:
     ```go
     baseURL := getEnv("WEBHOOK_BASE_URL", "")
     if baseURL == "" {
         sendErrorWithCORS(w, "WEBHOOK_BASE_URL must be configured", http.StatusInternalServerError)
         return
     }
     redirectURI := strings.TrimRight(baseURL, "/") + "/api/integrations/todoist/oauth/callback"
     
     // Генерируем state с user_id
     state := generateOAuthState(userID, jwtSecret)  // JWT с user_id и exp
     
     // Формируем URL для редиректа
     authURL := fmt.Sprintf(
         "https://todoist.com/oauth/authorize?client_id=%s&scope=data:read_write&state=%s&redirect_uri=%s",
         url.QueryEscape(todoistClientID),
         url.QueryEscape(state),
         url.QueryEscape(redirectURI),
     )
     
     http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
     ```
   
2. **OAuth callback:**
   - `GET /api/integrations/todoist/oauth/callback` - обрабатывает callback от Todoist
   - **ПУБЛИЧНЫЙ ENDPOINT** (без авторизации, так как пользователь приходит от Todoist)
   - Логика:
     1. Проверяет `state` параметр (JWT с user_id, exp = 1 день)
     2. Извлекает `code` из query parameters
     3. Обменивает `code` на `access_token` через POST запрос к Todoist
     4. Получает информацию о пользователе через Sync API
     5. Сохраняет/обновляет данные в БД
     6. Перенаправляет пользователя на страницу интеграций
   
   ```go
   func (a *App) todoistOAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
       frontendURL := getEnv("WEBHOOK_BASE_URL", "")
       redirectSuccess := frontendURL + "/?integration=todoist&status=connected"
       redirectError := frontendURL + "/?integration=todoist&status=error"
       
       // 1. Проверяем state (JWT с user_id, exp = 1 день)
       state := r.URL.Query().Get("state")
       userID, err := validateOAuthState(state, jwtSecret)
       if err != nil {
           log.Printf("Todoist OAuth: invalid state: %v", err)
           http.Redirect(w, r, redirectError+"&message=invalid_state", http.StatusTemporaryRedirect)
           return
       }
       
       // 2. Получаем code
       code := r.URL.Query().Get("code")
       if code == "" {
           log.Printf("Todoist OAuth: no code in callback")
           http.Redirect(w, r, redirectError+"&message=no_code", http.StatusTemporaryRedirect)
           return
       }
       
       // 3. Обмениваем code на access_token
       accessToken, err := exchangeCodeForToken(code, redirectURI)
       if err != nil {
           log.Printf("Todoist OAuth: token exchange failed: %v", err)
           http.Redirect(w, r, redirectError+"&message=token_exchange_failed", http.StatusTemporaryRedirect)
           return
       }
       
       // 4. Получаем информацию о пользователе
       todoistUser, err := getTodoistUserInfo(accessToken)
       if err != nil {
           log.Printf("Todoist OAuth: get user info failed: %v", err)
           http.Redirect(w, r, redirectError+"&message=user_info_failed", http.StatusTemporaryRedirect)
           return
       }
       
       log.Printf("Todoist OAuth: user_id=%d connected todoist_user_id=%d email=%s", 
           userID, todoistUser.ID, todoistUser.Email)
       
       // 5. Сохраняем в БД (INSERT или UPDATE)
       _, err = a.DB.Exec(`
           INSERT INTO todoist_integrations (user_id, todoist_user_id, todoist_email, access_token)
           VALUES ($1, $2, $3, $4)
           ON CONFLICT (user_id) DO UPDATE SET
               todoist_user_id = $2,
               todoist_email = $3,
               access_token = $4,
               updated_at = CURRENT_TIMESTAMP
       `, userID, todoistUser.ID, todoistUser.Email, accessToken)
       
       if err != nil {
           log.Printf("Todoist OAuth: DB error: %v", err)
           http.Redirect(w, r, redirectError+"&message=db_error", http.StatusTemporaryRedirect)
           return
       }
       
       // 6. Редирект на страницу интеграций
       http.Redirect(w, r, redirectSuccess, http.StatusTemporaryRedirect)
   }
   ```

3. **Получение статуса интеграции:**
   - `GET /api/integrations/todoist/status` - возвращает статус подключения
   - Требует авторизацию (protected endpoint)
   - Возвращает:
     ```json
     {
       "connected": true,
       "todoist_email": "user@example.com"
     }
     ```
     или если не подключено:
     ```json
     {
       "connected": false
     }
     ```
   - **Примечание:** webhook_url больше не нужен — он единый для всего приложения!

4. **Отключение интеграции:**
   - `DELETE /api/integrations/todoist/disconnect` - отключает интеграцию
   - Требует авторизацию (protected endpoint)
   - **Удаляет запись** из `todoist_integrations` полностью
   - Возвращает: `{"success": true, "message": "Todoist disconnected"}`

### 3.5. Новые маршруты:
```go
// OAuth endpoints
protected.HandleFunc("/api/integrations/todoist/oauth/connect", app.todoistOAuthConnectHandler).Methods("GET")
r.HandleFunc("/api/integrations/todoist/oauth/callback", app.todoistOAuthCallbackHandler).Methods("GET")  // Публичный!
protected.HandleFunc("/api/integrations/todoist/status", app.getTodoistStatusHandler).Methods("GET", "OPTIONS")
protected.HandleFunc("/api/integrations/todoist/disconnect", app.todoistDisconnectHandler).Methods("DELETE", "OPTIONS")

// Webhook (единый для всего приложения)
r.HandleFunc("/webhook/todoist", app.todoistWebhookHandler).Methods("POST", "OPTIONS")

// УДАЛИТЬ старый endpoint:
// protected.HandleFunc("/api/integrations/todoist/webhook-url", ...)  // Больше не нужен!
```

**Важно:** 
- OAuth callback должен быть публичным (пользователь приходит от Todoist без JWT)
- Webhook тоже публичный (Todoist отправляет события)
- `/api/integrations/todoist/webhook-url` — **УДАЛИТЬ**, больше не нужен!

---

## 4. Изменения в Frontend (TodoistIntegration.jsx)

### 4.1. Добавить проверку статуса подключения:
- При загрузке компонента вызывать `GET /api/integrations/todoist/status`
- Определять, подключен ли Todoist

### 4.2. Добавить OAuth flow:
- **Если не подключено:**
  - Показать кнопку "Подключить Todoist"
  - При клике: `window.location.href = '/api/integrations/todoist/oauth/connect'`
  - После OAuth callback backend перенаправит на `/?integration=todoist&status=connected`
  - При загрузке проверять URL параметры и показывать соответствующее сообщение

- **Если подключено:**
  - Показать email пользователя Todoist
  - Показать статус: "✅ Todoist подключен"
  - Кнопка "Отключить Todoist" (вызывает `DELETE /api/integrations/todoist/disconnect`)
  - **Webhook URL не нужен** — всё работает автоматически!

### 4.3. Обновить инструкции:
- **Если не подключено:**
  - Инструкция: "Нажмите кнопку 'Подключить Todoist' для авторизации"

- **Если подключено:**
  - Инструкция: "✅ Todoist подключен! Закрывайте задачи в Todoist — они автоматически появятся в Play Life."
  - **Никаких дополнительных настроек не требуется!**

### 4.4. Удалить:
- Отображение webhook URL
- Кнопку "Копировать"
- Инструкции по настройке webhook в Todoist

---

## 5. Порядок выполнения изменений

### Шаг 1: Создать миграцию БД
- Создать файл `013_refactor_todoist_single_app.sql`
- Содержимое миграции:
```sql
-- Migration: Refactor todoist_integrations for single Todoist app
-- Webhook теперь единый для всего приложения, токены в URL больше не нужны

-- 1. Добавляем новые поля
ALTER TABLE todoist_integrations 
ADD COLUMN IF NOT EXISTS todoist_user_id BIGINT;

ALTER TABLE todoist_integrations 
ADD COLUMN IF NOT EXISTS todoist_email VARCHAR(255);

ALTER TABLE todoist_integrations 
ADD COLUMN IF NOT EXISTS access_token TEXT;

-- 2. Удаляем webhook_token (больше не нужен!)
ALTER TABLE todoist_integrations 
DROP COLUMN IF EXISTS webhook_token;

-- 3. Удаляем старый индекс на webhook_token
DROP INDEX IF EXISTS idx_todoist_integrations_webhook_token;

-- 4. Создаем новые индексы
CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_todoist_user_id 
ON todoist_integrations(todoist_user_id) 
WHERE todoist_user_id IS NOT NULL;

CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_todoist_email 
ON todoist_integrations(todoist_email) 
WHERE todoist_email IS NOT NULL;

-- 5. Комментарии
COMMENT ON COLUMN todoist_integrations.todoist_user_id IS 'Todoist user ID (from OAuth) - used to identify user in webhooks';
COMMENT ON COLUMN todoist_integrations.todoist_email IS 'Todoist user email (from OAuth)';
COMMENT ON COLUMN todoist_integrations.access_token IS 'Todoist OAuth access token (permanent)';
```
- Применить миграцию

**Важно:** После миграции старые записи с `webhook_token` будут работать пока не применится миграция. После миграции все пользователи должны переподключить Todoist через OAuth.

### Шаг 2: Обновить .env
- Добавить новые переменные окружения
- Получить данные из Todoist приложения

### Шаг 3: Обновить Backend
- Обновить структуру `TodoistIntegration`
- Изменить webhook handler
- Добавить OAuth endpoints
- Обновить маршруты

### Шаг 4: Обновить Frontend
- Обновить компонент `TodoistIntegration.jsx`
- Добавить OAuth flow

### Шаг 5: Тестирование
- Протестировать OAuth flow
- Протестировать webhook с новым способом идентификации
- Проверить миграцию данных

---

## 6. Важные замечания

### 6.1. Идентификация пользователя в webhook
**Новый подход:**
- Используется `todoist_user_id` из `event_data` webhook
- `todoist_user_id` сохраняется при OAuth подключении
- Webhook приходит на единый URL `/webhook/todoist`
- Находим пользователя Play Life по `todoist_user_id`

### 6.2. Миграция существующих данных
- **Удаляем `webhook_token`** — больше не нужен
- Все существующие записи будут работать после миграции, но без OAuth данных
- Пользователям нужно **переподключить Todoist через OAuth** для работы интеграции
- После миграции старый endpoint `/webhook/todoist/{token}` перестанет работать

### 6.3. Обратная совместимость
- **НЕТ обратной совместимости** — это breaking change
- Старый endpoint `/webhook/todoist/{token}` удаляется
- Все пользователи должны переподключить Todoist
- **Рекомендация:** Уведомить пользователей о необходимости переподключения

### 6.3.1. Удаляемый код
**Удалить полностью:**
- Endpoint `GET /api/integrations/todoist/webhook-url`
- Handler `getTodoistWebhookURLHandler`
- Маршрут `/webhook/todoist/{token}`
- Функция генерации webhook_token для Todoist

### 6.4. Безопасность
- OAuth токен (`access_token`) не отдавать в JSON ответах (json:"-")
- Использовать `TODOIST_WEBHOOK_SECRET` для проверки подлинности webhook (если настроен в Todoist)
- Todoist access_token бессрочный, но пользователь может отозвать его в настройках Todoist
- User-Agent для запросов к Todoist API: `PlayLife`

### 6.5. OAuth Flow (детально)
1. Пользователь нажимает "Подключить Todoist"
2. Backend генерирует `state` (случайная строка или JWT с user_id) и сохраняет его
3. Перенаправление на Todoist OAuth:
   ```
   https://todoist.com/oauth/authorize?
     client_id=<TODOIST_CLIENT_ID>&
     scope=data:read_write&
     state=<state>&
     redirect_uri=<WEBHOOK_BASE_URL>/api/integrations/todoist/oauth/callback
   ```
4. Пользователь авторизуется в Todoist
5. Todoist перенаправляет на `redirect_uri` с `code` и `state`
6. Backend проверяет `state` и обменивает `code` на `access_token`:
   ```
   POST https://todoist.com/oauth/access_token
   Content-Type: application/x-www-form-urlencoded
   
   client_id=<TODOIST_CLIENT_ID>&
   client_secret=<TODOIST_CLIENT_SECRET>&
   code=<code>&
   redirect_uri=<redirect_uri>
   ```
7. Backend получает информацию о пользователе через Todoist Sync API:
   ```
   POST https://api.todoist.com/sync/v9/sync
   Authorization: Bearer <access_token>
   Content-Type: application/x-www-form-urlencoded
   User-Agent: PlayLife
   
   sync_token=*&resource_types=["user"]
   ```
   Ответ содержит `user.id` и `user.email`
8. Backend сохраняет `todoist_user_id`, `todoist_email`, `access_token` в БД
9. Перенаправление пользователя на страницу интеграций

### 6.6. Хранение state для OAuth
Используем JWT токен (не требует хранения в БД):

```go
// Генерация state (таймаут = 1 день)
func generateOAuthState(userID int, jwtSecret string) string {
    state := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "user_id": userID,
        "type":    "todoist_oauth",
        "exp":     time.Now().Add(24 * time.Hour).Unix(),  // 1 день
    })
    stateString, _ := state.SignedString([]byte(jwtSecret))
    return stateString
}

// Проверка state в callback
func validateOAuthState(stateString string, jwtSecret string) (int, error) {
    token, err := jwt.Parse(stateString, func(token *jwt.Token) (interface{}, error) {
        return []byte(jwtSecret), nil
    })
    if err != nil {
        return 0, err
    }
    
    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || !token.Valid {
        return 0, fmt.Errorf("invalid token")
    }
    
    if claims["type"] != "todoist_oauth" {
        return 0, fmt.Errorf("wrong token type")
    }
    
    userID := int(claims["user_id"].(float64))
    return userID, nil
}
```

### 6.7. Особенности Todoist OAuth
- **Scope:** `data:read_write` — полный доступ к данным пользователя
- **Access Token:** Todoist выдает бессрочный access_token
- **Refresh Token:** Todoist НЕ использует refresh_token
- **Отзыв токена:** Пользователь может отозвать доступ в настройках Todoist

### 6.8. Обработка ошибок

**Если todoist_user_id не найден в webhook:**
- Логировать: `log.Printf("Todoist webhook: no user found for todoist_user_id=%d", todoistUserID)`
- Возвращать `200 OK` (чтобы Todoist не делал retry)
- Игнорировать событие

**Если токен отозван пользователем:**
- При попытке использовать access_token Todoist вернет ошибку
- Автоматически отключить интеграцию (удалить запись из БД)
- Логировать: `log.Printf("Todoist: token revoked for user_id=%d, disconnecting", userID)`

**При disconnect:**
- Просто удалить запись из БД
- НЕ отзывать токен через Todoist API (упрощение)

### 6.9. События Todoist
Подписываемся только на: **`item:completed`**

Другие события (`item:added`, `item:updated`, `item:deleted`) не нужны.

---

## 7. Архитектура: Единый Webhook

**Ключевое решение:** Используем единый webhook URL для всего приложения.

### Как это работает:

1. **Настройка в Todoist Developer Console:**
   - Создать приложение в https://developer.todoist.com/appconsole.html
   - Указать Webhook URL: `<WEBHOOK_BASE_URL>/webhook/todoist`
   - Указать OAuth Redirect URI: `<WEBHOOK_BASE_URL>/api/integrations/todoist/oauth/callback`
   - Выбрать события: `item:completed`

2. **При OAuth подключении:**
   - Пользователь нажимает "Подключить Todoist"
   - Авторизуется в Todoist
   - Play Life получает `access_token` и информацию о пользователе
   - Сохраняем `todoist_user_id` — это ключ для идентификации в webhook

3. **При получении webhook:**
   - Todoist отправляет POST на `/webhook/todoist`
   - В `event_data` есть `user_id` (это Todoist user_id)
   - Находим пользователя Play Life по `todoist_user_id`
   - Обрабатываем событие

### Преимущества:
- ✅ Пользователю не нужно ничего настраивать!
- ✅ Нет токенов в URL
- ✅ Простая архитектура
- ✅ Webhook настраивается один раз в Developer Console

---

## 8. Настройка Todoist приложения в Developer Console

### Шаги настройки:
1. Зайти в https://developer.todoist.com/appconsole.html
2. Создать новое приложение или открыть существующее
3. Заполнить:
   - **App name:** Play Life
   - **App description:** Интеграция с Play Life для отслеживания прогресса
   - **OAuth Redirect URL:** `<WEBHOOK_BASE_URL>/api/integrations/todoist/oauth/callback`
   - **Webhooks callback URL:** `<WEBHOOK_BASE_URL>/webhook/todoist`
   - **Watched events:** `item:completed` (только это событие!)
4. Скопировать:
   - **Client ID** → `TODOIST_CLIENT_ID`
   - **Client Secret** → `TODOIST_CLIENT_SECRET`
   - **Client secret for webhooks** (если есть) → `TODOIST_WEBHOOK_SECRET`

### Важные настройки:
- **OAuth scope:** `data:read_write`
- **Watched events:** только `item:completed`
- Другие события НЕ подписывать

### Формат webhook от Todoist:
```json
{
  "event_name": "item:completed",
  "user_id": "12345678",  // ← Это todoist_user_id для идентификации!
  "event_data": {
    "id": "task_id",
    "content": "Task title",
    "description": "Task description",
    "user_id": "12345678",  // ← Тоже здесь
    ...
  }
}
```

**Важно:** `user_id` приходит как string, нужно конвертировать в int64.

---

## 9. Краткая сводка для быстрого старта

### Настройка Todoist приложения:
1. Зайти в https://developer.todoist.com/appconsole.html
2. Создать приложение
3. Настроить:
   - **OAuth Redirect URL:** `<WEBHOOK_BASE_URL>/api/integrations/todoist/oauth/callback`
   - **Webhooks callback URL:** `<WEBHOOK_BASE_URL>/webhook/todoist`
   - **Watched events:** `item:completed`
4. Скопировать Client ID и Client Secret

### Что добавить в .env:
```env
TODOIST_CLIENT_ID=your-client-id-here
TODOIST_CLIENT_SECRET=your-client-secret-here
TODOIST_WEBHOOK_SECRET=  # опционально, из Developer Console
```

### Что изменится в базе данных:
- Добавятся поля: `todoist_user_id`, `todoist_email`, `access_token`
- **Удалится поле:** `webhook_token`

### Что изменится для пользователей:
- Пользователи нажимают "Подключить Todoist"
- Авторизуются в Todoist
- **Готово!** Никаких дополнительных настроек!
- Закрытые задачи в Todoist автоматически появляются в Play Life

### Порядок реализации:
1. ⬜ Настроить Todoist приложение в Developer Console
2. ⬜ Создать миграцию БД (`013_refactor_todoist_single_app.sql`)
3. ⬜ Обновить `.env` с новыми переменными
4. ⬜ Реализовать OAuth endpoints в Backend
5. ⬜ Обновить webhook handler (идентификация по todoist_user_id)
6. ⬜ Обновить Frontend компонент
7. ⬜ Удалить старый код (webhook-url endpoint, токены)
8. ⬜ Протестировать OAuth flow и webhook

