4.27.2: Улучшение отладки OAuth Fitbit
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:
poignatov
2026-02-06 21:15:08 +03:00
parent af2aaa4168
commit d355928aa9
4 changed files with 66 additions and 16 deletions

View File

@@ -1 +1 @@
4.27.1 4.27.2

View File

@@ -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)
} }

View File

@@ -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",

View File

@@ -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 успешно подключен!')
// Очищаем URL параметры
window.history.replaceState({}, '', window.location.pathname)
} else if (status === 'error') { } else if (status === 'error') {
const errorMsg = params.get('message') || 'Произошла ошибка' const errorMsg = params.get('message') || 'unknown_error'
setToastMessage({ text: errorMsg, type: 'error' }) const errorMessages = {
window.history.replaceState({}, '', window.location.pathname) '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(() => { 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">