- Single webhook URL for all users - OAuth authorization flow - Removed individual webhook tokens - User identification by todoist_user_id - Added OAuth endpoints: connect, callback, status, disconnect - Updated frontend with OAuth flow - DB migration 013: removed webhook_token, added todoist_user_id, todoist_email, access_token Version: 2.2.0
32 KiB
План рефакторинга интеграции с 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_datawebhook - 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:
-
Удалить:
webhook_token— больше не нужен! Webhook единый для всего приложения.
-
Добавить:
todoist_user_id(BIGINT) — ID пользователя в Todoist (из OAuth, для идентификации в webhook)todoist_email(VARCHAR(255)) — Email пользователя в Todoist (из OAuth)access_token(TEXT) — OAuth access token (бессрочный в Todoist)
-
Индексы:
- Уникальный индекс на
todoist_user_id— ключевой для идентификации в webhook! - Уникальный индекс на
todoist_email - Удалить индекс на
webhook_token
- Уникальный индекс на
Структура после миграции:
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:
# ============================================
# 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 приложения:
TODOIST_CLIENT_ID- Client ID приложенияTODOIST_CLIENT_SECRET- Client Secret приложения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:
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_datawebhook - Находит пользователя по
todoist_user_id
Новая логика:
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 - ИЗМЕНИТЬ:
// Было:
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:
-
Инициация OAuth:
GET /api/integrations/todoist/oauth/connect- перенаправляет на Todoist OAuth- ВАЖНО: Требует авторизацию пользователя (JWT token в cookie или header)
- Генерирует
stateпараметр с user_id (JWT подписанный jwtSecret) - Формирует
redirect_uriизWEBHOOK_BASE_URL: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)
-
OAuth callback:
GET /api/integrations/todoist/oauth/callback- обрабатывает callback от Todoist- ПУБЛИЧНЫЙ ENDPOINT (без авторизации, так как пользователь приходит от Todoist)
- Логика:
- Проверяет
stateпараметр (JWT с user_id, exp = 1 день) - Извлекает
codeиз query parameters - Обменивает
codeнаaccess_tokenчерез POST запрос к Todoist - Получает информацию о пользователе через Sync API
- Сохраняет/обновляет данные в БД
- Перенаправляет пользователя на страницу интеграций
- Проверяет
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) } -
Получение статуса интеграции:
GET /api/integrations/todoist/status- возвращает статус подключения- Требует авторизацию (protected endpoint)
- Возвращает:
или если не подключено:
{ "connected": true, "todoist_email": "user@example.com" }{ "connected": false } - Примечание: webhook_url больше не нужен — он единый для всего приложения!
-
Отключение интеграции:
DELETE /api/integrations/todoist/disconnect- отключает интеграцию- Требует авторизацию (protected endpoint)
- Удаляет запись из
todoist_integrationsполностью - Возвращает:
{"success": true, "message": "Todoist disconnected"}
3.5. Новые маршруты:
// 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 - Содержимое миграции:
-- 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_datawebhook 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 (детально)
- Пользователь нажимает "Подключить Todoist"
- Backend генерирует
state(случайная строка или JWT с user_id) и сохраняет его - Перенаправление на 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 - Пользователь авторизуется в Todoist
- Todoist перенаправляет на
redirect_uriсcodeиstate - 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> - 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 - Backend сохраняет
todoist_user_id,todoist_email,access_tokenв БД - Перенаправление пользователя на страницу интеграций
6.6. Хранение state для OAuth
Используем JWT токен (не требует хранения в БД):
// Генерация 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 для всего приложения.
Как это работает:
-
Настройка в 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
-
При OAuth подключении:
- Пользователь нажимает "Подключить Todoist"
- Авторизуется в Todoist
- Play Life получает
access_tokenи информацию о пользователе - Сохраняем
todoist_user_id— это ключ для идентификации в webhook
-
При получении webhook:
- Todoist отправляет POST на
/webhook/todoist - В
event_dataестьuser_id(это Todoist user_id) - Находим пользователя Play Life по
todoist_user_id - Обрабатываем событие
- Todoist отправляет POST на
Преимущества:
- ✅ Пользователю не нужно ничего настраивать!
- ✅ Нет токенов в URL
- ✅ Простая архитектура
- ✅ Webhook настраивается один раз в Developer Console
8. Настройка Todoist приложения в Developer Console
Шаги настройки:
- Зайти в https://developer.todoist.com/appconsole.html
- Создать новое приложение или открыть существующее
- Заполнить:
- 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(только это событие!)
- Скопировать:
- Client ID →
TODOIST_CLIENT_ID - Client Secret →
TODOIST_CLIENT_SECRET - Client secret for webhooks (если есть) →
TODOIST_WEBHOOK_SECRET
- Client ID →
Важные настройки:
- OAuth scope:
data:read_write - Watched events: только
item:completed - Другие события НЕ подписывать
Формат webhook от Todoist:
{
"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 приложения:
- Зайти в https://developer.todoist.com/appconsole.html
- Создать приложение
- Настроить:
- OAuth Redirect URL:
<WEBHOOK_BASE_URL>/api/integrations/todoist/oauth/callback - Webhooks callback URL:
<WEBHOOK_BASE_URL>/webhook/todoist - Watched events:
item:completed
- OAuth Redirect URL:
- Скопировать Client ID и Client Secret
Что добавить в .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
Порядок реализации:
- ⬜ Настроить Todoist приложение в Developer Console
- ⬜ Создать миграцию БД (
013_refactor_todoist_single_app.sql) - ⬜ Обновить
.envс новыми переменными - ⬜ Реализовать OAuth endpoints в Backend
- ⬜ Обновить webhook handler (идентификация по todoist_user_id)
- ⬜ Обновить Frontend компонент
- ⬜ Удалить старый код (webhook-url endpoint, токены)
- ⬜ Протестировать OAuth flow и webhook