# План рефакторинга интеграции с 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: - Используйте: `/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=& scope=data:read_write& state=& redirect_uri=/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=& client_secret=& code=& redirect_uri= ``` 7. Backend получает информацию о пользователе через Todoist Sync API: ``` POST https://api.todoist.com/sync/v9/sync Authorization: Bearer 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/todoist` - Указать OAuth Redirect URI: `/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:** `/api/integrations/todoist/oauth/callback` - **Webhooks callback 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:** `/api/integrations/todoist/oauth/callback` - **Webhooks callback 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