4.27.2: Улучшение отладки OAuth Fitbit
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m24s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m24s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10367,6 +10367,8 @@ func (a *App) fitbitOAuthConnectHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// fitbitOAuthCallbackHandler обрабатывает OAuth callback от Fitbit
|
// fitbitOAuthCallbackHandler обрабатывает OAuth callback от Fitbit
|
||||||
func (a *App) fitbitOAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
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", "")
|
frontendURL := getEnv("WEBHOOK_BASE_URL", "")
|
||||||
redirectSuccess := frontendURL + "/?integration=fitbit&status=connected"
|
redirectSuccess := frontendURL + "/?integration=fitbit&status=connected"
|
||||||
redirectError := frontendURL + "/?integration=fitbit&status=error"
|
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", "")
|
clientSecret := getEnv("FITBIT_CLIENT_SECRET", "")
|
||||||
baseURL := getEnv("WEBHOOK_BASE_URL", "")
|
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 == "" {
|
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)
|
http.Redirect(w, r, redirectError+"&message=config_error", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURI := strings.TrimRight(baseURL, "/") + "/api/integrations/fitbit/oauth/callback"
|
redirectURI := strings.TrimRight(baseURL, "/") + "/api/integrations/fitbit/oauth/callback"
|
||||||
|
log.Printf("Fitbit OAuth callback: redirectURI=%s", redirectURI)
|
||||||
|
|
||||||
// Проверяем state
|
// Проверяем state
|
||||||
state := r.URL.Query().Get("state")
|
state := r.URL.Query().Get("state")
|
||||||
userID, err := validateFitbitOAuthState(state, a.jwtSecret)
|
userID, err := validateFitbitOAuthState(state, a.jwtSecret)
|
||||||
if err != nil {
|
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)
|
http.Redirect(w, r, redirectError+"&message=invalid_state", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Fitbit OAuth callback: validated state, user_id=%d", userID)
|
||||||
|
|
||||||
// Получаем code
|
// Получаем code
|
||||||
code := r.URL.Query().Get("code")
|
code := r.URL.Query().Get("code")
|
||||||
if 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)
|
http.Redirect(w, r, redirectError+"&message=no_code", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Fitbit OAuth callback: got code, exchanging for tokens...")
|
||||||
|
|
||||||
// Обмениваем code на токены
|
// Обмениваем code на токены
|
||||||
accessToken, refreshToken, expiresIn, err := exchangeFitbitCodeForToken(code, redirectURI, clientID, clientSecret)
|
accessToken, refreshToken, expiresIn, err := exchangeFitbitCodeForToken(code, redirectURI, clientID, clientSecret)
|
||||||
if err != nil {
|
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)
|
http.Redirect(w, r, redirectError+"&message=token_exchange_failed", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Fitbit OAuth callback: token exchange successful, expiresIn=%d", expiresIn)
|
||||||
|
|
||||||
// Получаем информацию о пользователе
|
// Получаем информацию о пользователе
|
||||||
fitbitUserID, err := getFitbitUserInfo(accessToken)
|
fitbitUserID, err := getFitbitUserInfo(accessToken)
|
||||||
if err != nil {
|
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)
|
http.Redirect(w, r, redirectError+"&message=user_info_failed", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -10434,11 +10450,13 @@ func (a *App) fitbitOAuthCallbackHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
`, userID, fitbitUserID, accessToken, refreshToken, tokenExpiresAt)
|
`, userID, fitbitUserID, accessToken, refreshToken, tokenExpiresAt)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
http.Redirect(w, r, redirectError+"&message=db_error", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Fitbit OAuth: successfully saved integration for user_id=%d, redirecting to %s", userID, redirectSuccess)
|
||||||
|
|
||||||
// Редирект на страницу интеграций
|
// Редирект на страницу интеграций
|
||||||
http.Redirect(w, r, redirectSuccess, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, redirectSuccess, http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "4.27.1",
|
"version": "4.27.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ function FitbitIntegration({ onNavigate }) {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
|
const [oauthError, setOauthError] = useState('')
|
||||||
const [toastMessage, setToastMessage] = useState(null)
|
const [toastMessage, setToastMessage] = useState(null)
|
||||||
const [isLoadingError, setIsLoadingError] = useState(false)
|
const [isLoadingError, setIsLoadingError] = useState(false)
|
||||||
const [goals, setGoals] = useState({
|
const [goals, setGoals] = useState({
|
||||||
@@ -26,23 +27,35 @@ function FitbitIntegration({ onNavigate }) {
|
|||||||
const [editedGoals, setEditedGoals] = useState(goals)
|
const [editedGoals, setEditedGoals] = useState(goals)
|
||||||
const [syncing, setSyncing] = useState(false)
|
const [syncing, setSyncing] = useState(false)
|
||||||
|
|
||||||
|
// Сохраняем OAuth статус из URL в ref, чтобы проверить после checkStatus
|
||||||
|
const oauthStatusRef = React.useRef(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkStatus()
|
// Проверяем URL параметры для сообщений ДО вызова checkStatus
|
||||||
// Проверяем URL параметры для сообщений
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const integration = params.get('integration')
|
const integration = params.get('integration')
|
||||||
const status = params.get('status')
|
const status = params.get('status')
|
||||||
if (integration === 'fitbit') {
|
if (integration === 'fitbit') {
|
||||||
|
oauthStatusRef.current = status
|
||||||
if (status === 'connected') {
|
if (status === 'connected') {
|
||||||
setMessage('✅ Fitbit успешно подключен!')
|
setMessage('Fitbit успешно подключен!')
|
||||||
|
} else if (status === 'error') {
|
||||||
|
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 параметры
|
// Очищаем URL параметры
|
||||||
window.history.replaceState({}, '', window.location.pathname)
|
window.history.replaceState({}, '', window.location.pathname)
|
||||||
} else if (status === 'error') {
|
|
||||||
const errorMsg = params.get('message') || 'Произошла ошибка'
|
|
||||||
setToastMessage({ text: errorMsg, type: 'error' })
|
|
||||||
window.history.replaceState({}, '', window.location.pathname)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
checkStatus()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -65,6 +78,12 @@ function FitbitIntegration({ onNavigate }) {
|
|||||||
setGoals(data.goals)
|
setGoals(data.goals)
|
||||||
setEditedGoals(data.goals)
|
setEditedGoals(data.goals)
|
||||||
}
|
}
|
||||||
|
// Если OAuth вернул status=connected, но бэкенд не подтвердил подключение
|
||||||
|
if (oauthStatusRef.current === 'connected' && !data.connected) {
|
||||||
|
setOauthError('Авторизация в Fitbit прошла, но подключение не сохранилось. Попробуйте ещё раз или обратитесь к администратору.')
|
||||||
|
setMessage('')
|
||||||
|
}
|
||||||
|
oauthStatusRef.current = null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking status:', error)
|
console.error('Error checking status:', error)
|
||||||
setError(error.message || 'Не удалось проверить статус')
|
setError(error.message || 'Не удалось проверить статус')
|
||||||
@@ -250,6 +269,12 @@ function FitbitIntegration({ onNavigate }) {
|
|||||||
<p className="text-green-800">{message}</p>
|
<p className="text-green-800">{message}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{oauthError && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-red-800">{oauthError}</p>
|
||||||
|
<button onClick={() => setOauthError('')} className="text-red-600 text-sm underline mt-1">Скрыть</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Статистика */}
|
{/* Статистика */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
@@ -440,6 +465,13 @@ function FitbitIntegration({ onNavigate }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
|
{oauthError && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-red-800 font-medium">Ошибка подключения Fitbit</p>
|
||||||
|
<p className="text-red-700 mt-1">{oauthError}</p>
|
||||||
|
<button onClick={() => setOauthError('')} className="text-red-600 text-sm underline mt-2">Скрыть</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">Подключение Fitbit</h2>
|
<h2 className="text-lg font-semibold mb-4">Подключение Fitbit</h2>
|
||||||
<p className="text-gray-700 mb-4">
|
<p className="text-gray-700 mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user