diff --git a/VERSION b/VERSION index 4c42b8c..4b73353 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.27.1 +4.27.2 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 0afe99f..2e6db7d 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -10367,6 +10367,8 @@ func (a *App) fitbitOAuthConnectHandler(w http.ResponseWriter, r *http.Request) // fitbitOAuthCallbackHandler обрабатывает OAuth callback от Fitbit func (a *App) fitbitOAuthCallbackHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("Fitbit OAuth callback: received request, URL=%s", r.URL.String()) + frontendURL := getEnv("WEBHOOK_BASE_URL", "") redirectSuccess := frontendURL + "/?integration=fitbit&status=connected" redirectError := frontendURL + "/?integration=fitbit&status=error" @@ -10375,43 +10377,57 @@ func (a *App) fitbitOAuthCallbackHandler(w http.ResponseWriter, r *http.Request) clientSecret := getEnv("FITBIT_CLIENT_SECRET", "") baseURL := getEnv("WEBHOOK_BASE_URL", "") + log.Printf("Fitbit OAuth callback: WEBHOOK_BASE_URL=%s, FITBIT_CLIENT_ID set=%v, FITBIT_CLIENT_SECRET set=%v", + baseURL, clientID != "", clientSecret != "") + if clientID == "" || clientSecret == "" || baseURL == "" { - log.Printf("Fitbit OAuth: missing configuration") + log.Printf("Fitbit OAuth: missing configuration (clientID=%v, clientSecret=%v, baseURL=%v)", + clientID != "", clientSecret != "", baseURL != "") http.Redirect(w, r, redirectError+"&message=config_error", http.StatusTemporaryRedirect) return } redirectURI := strings.TrimRight(baseURL, "/") + "/api/integrations/fitbit/oauth/callback" + log.Printf("Fitbit OAuth callback: redirectURI=%s", redirectURI) // Проверяем state state := r.URL.Query().Get("state") userID, err := validateFitbitOAuthState(state, a.jwtSecret) if err != nil { - log.Printf("Fitbit OAuth: invalid state: %v", err) + log.Printf("Fitbit OAuth: invalid state: %v (state length=%d)", err, len(state)) http.Redirect(w, r, redirectError+"&message=invalid_state", http.StatusTemporaryRedirect) return } + log.Printf("Fitbit OAuth callback: validated state, user_id=%d", userID) + // Получаем code code := r.URL.Query().Get("code") if code == "" { - log.Printf("Fitbit OAuth: no code in callback") + // Проверяем наличие ошибки от Fitbit + fitbitError := r.URL.Query().Get("error") + fitbitErrorDesc := r.URL.Query().Get("error_description") + log.Printf("Fitbit OAuth: no code in callback, error=%s, error_description=%s", fitbitError, fitbitErrorDesc) http.Redirect(w, r, redirectError+"&message=no_code", http.StatusTemporaryRedirect) return } + log.Printf("Fitbit OAuth callback: got code, exchanging for tokens...") + // Обмениваем code на токены accessToken, refreshToken, expiresIn, err := exchangeFitbitCodeForToken(code, redirectURI, clientID, clientSecret) if err != nil { - log.Printf("Fitbit OAuth: token exchange failed: %v", err) + log.Printf("Fitbit OAuth: token exchange failed for user_id=%d: %v", userID, err) http.Redirect(w, r, redirectError+"&message=token_exchange_failed", http.StatusTemporaryRedirect) return } + log.Printf("Fitbit OAuth callback: token exchange successful, expiresIn=%d", expiresIn) + // Получаем информацию о пользователе fitbitUserID, err := getFitbitUserInfo(accessToken) if err != nil { - log.Printf("Fitbit OAuth: get user info failed: %v", err) + log.Printf("Fitbit OAuth: get user info failed for user_id=%d: %v", userID, err) http.Redirect(w, r, redirectError+"&message=user_info_failed", http.StatusTemporaryRedirect) return } @@ -10434,11 +10450,13 @@ func (a *App) fitbitOAuthCallbackHandler(w http.ResponseWriter, r *http.Request) `, userID, fitbitUserID, accessToken, refreshToken, tokenExpiresAt) if err != nil { - log.Printf("Fitbit OAuth: DB error: %v", err) + log.Printf("Fitbit OAuth: DB error for user_id=%d: %v", userID, err) http.Redirect(w, r, redirectError+"&message=db_error", http.StatusTemporaryRedirect) return } + log.Printf("Fitbit OAuth: successfully saved integration for user_id=%d, redirecting to %s", userID, redirectSuccess) + // Редирект на страницу интеграций http.Redirect(w, r, redirectSuccess, http.StatusTemporaryRedirect) } diff --git a/play-life-web/package.json b/play-life-web/package.json index f916b37..206968d 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "4.27.1", + "version": "4.27.2", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/FitbitIntegration.jsx b/play-life-web/src/components/FitbitIntegration.jsx index bbd44cf..83bb543 100644 --- a/play-life-web/src/components/FitbitIntegration.jsx +++ b/play-life-web/src/components/FitbitIntegration.jsx @@ -10,6 +10,7 @@ function FitbitIntegration({ onNavigate }) { const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [message, setMessage] = useState('') + const [oauthError, setOauthError] = useState('') const [toastMessage, setToastMessage] = useState(null) const [isLoadingError, setIsLoadingError] = useState(false) const [goals, setGoals] = useState({ @@ -26,23 +27,35 @@ function FitbitIntegration({ onNavigate }) { const [editedGoals, setEditedGoals] = useState(goals) const [syncing, setSyncing] = useState(false) + // Сохраняем OAuth статус из URL в ref, чтобы проверить после checkStatus + const oauthStatusRef = React.useRef(null) + useEffect(() => { - checkStatus() - // Проверяем URL параметры для сообщений + // Проверяем URL параметры для сообщений ДО вызова checkStatus const params = new URLSearchParams(window.location.search) const integration = params.get('integration') const status = params.get('status') if (integration === 'fitbit') { + oauthStatusRef.current = status if (status === 'connected') { - setMessage('✅ Fitbit успешно подключен!') - // Очищаем URL параметры - window.history.replaceState({}, '', window.location.pathname) + setMessage('Fitbit успешно подключен!') } else if (status === 'error') { - const errorMsg = params.get('message') || 'Произошла ошибка' - setToastMessage({ text: errorMsg, type: 'error' }) - window.history.replaceState({}, '', window.location.pathname) + const errorMsg = params.get('message') || 'unknown_error' + const errorMessages = { + 'config_error': 'Ошибка конфигурации сервера. Обратитесь к администратору.', + 'invalid_state': 'Недействительный токен авторизации. Попробуйте ещё раз.', + 'no_code': 'Не получен код авторизации от Fitbit. Попробуйте ещё раз.', + 'token_exchange_failed': 'Не удалось обменять код на токен. Проверьте настройки Fitbit приложения.', + 'user_info_failed': 'Не удалось получить информацию о пользователе Fitbit.', + 'db_error': 'Ошибка сохранения данных. Попробуйте ещё раз.', + 'unknown_error': 'Произошла неизвестная ошибка при подключении Fitbit.' + } + setOauthError(errorMessages[errorMsg] || `Ошибка: ${errorMsg}`) } + // Очищаем URL параметры + window.history.replaceState({}, '', window.location.pathname) } + checkStatus() }, []) useEffect(() => { @@ -65,6 +78,12 @@ function FitbitIntegration({ onNavigate }) { setGoals(data.goals) setEditedGoals(data.goals) } + // Если OAuth вернул status=connected, но бэкенд не подтвердил подключение + if (oauthStatusRef.current === 'connected' && !data.connected) { + setOauthError('Авторизация в Fitbit прошла, но подключение не сохранилось. Попробуйте ещё раз или обратитесь к администратору.') + setMessage('') + } + oauthStatusRef.current = null } catch (error) { console.error('Error checking status:', error) setError(error.message || 'Не удалось проверить статус') @@ -250,6 +269,12 @@ function FitbitIntegration({ onNavigate }) {
{message}
)} + {oauthError && ( +{oauthError}
+ +Ошибка подключения Fitbit
+{oauthError}
+ +