From 4bc28bc0f837cf6a2c15689ff6eb84ca0f2b9abb Mon Sep 17 00:00:00 2001 From: Max Techera Date: Thu, 19 Feb 2026 17:03:08 -0300 Subject: [PATCH 1/2] fix(SUPPORT-19): handle googleDriveOAuth2 v2 token refresh and re-auth in GoogleDrivePicker - Add getTokenFields() helper to read both v1 (googleAccessToken/expiresAt) and v2 (access_token/expires_at) credential field names - Route refresh to correct endpoint: v2 uses /oauth2-credential/refresh/:id instead of /credentials/refresh-token (which only works for v1) - Catch 401/REAUTH_REQUIRED and show 'Re-authenticate with Google' button - Add OAuth2 popup re-auth flow (same pattern as AddEditCredentialDialog) - Reset new states when credential changes --- .../ui-component/drive/GoogleDrivePicker.jsx | 128 +++++++++++++----- 1 file changed, 93 insertions(+), 35 deletions(-) diff --git a/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx b/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx index 4ea97572d42..f0e718e9d90 100644 --- a/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx +++ b/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx @@ -3,10 +3,19 @@ import { useEffect, useState, useCallback, useRef } from 'react' import { Button, List, ListItem, ListItemAvatar, ListItemText, Avatar, IconButton, Stack, Alert } from '@mui/material' import useApi from '@/hooks/useApi' import credentialsApi from '@/api/credentials' +import oauth2Api from '@/api/oauth2' import { IconX, IconTrash } from '@tabler/icons-react' import { useDispatch } from 'react-redux' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +// Extract token fields from credential data — handles both googleOAuth (v1) and googleDriveOAuth2 (v2) +const getTokenFields = (plainDataObj) => { + const token = plainDataObj.googleAccessToken || plainDataObj.access_token || '' + const expiryStr = plainDataObj.expiresAt || plainDataObj.expires_at + const isV2 = !!plainDataObj.access_token && !plainDataObj.googleAccessToken + return { token, expiryStr, isV2 } +} + export const SUPPORTED_MIME_TYPES = [ 'application/vnd.google-apps.document', 'application/vnd.google-apps.spreadsheet', @@ -176,6 +185,8 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre const [accessToken, setAccessToken] = useState(null) const [isTokenExpired, setIsTokenExpired] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) + const [isV2Credential, setIsV2Credential] = useState(false) + const [isReauthRequired, setIsReauthRequired] = useState(false) const enqueueSnackbar = useCallback((...args) => dispatch(enqueueSnackbarAction(...args)), [dispatch]) const closeSnackbar = useCallback((...args) => dispatch(closeSnackbarAction(...args)), [dispatch]) @@ -210,30 +221,30 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre // Reset token state setAccessToken(null) setIsTokenExpired(false) + setIsV2Credential(false) + setIsReauthRequired(false) } }, [credentialId]) // Handle credential data from prop useEffect(() => { if (credentialData?.plainDataObj) { - const expiresAt = new Date(credentialData.plainDataObj.expiresAt) - const now = new Date() - const isExpired = expiresAt < now - + const { token, expiryStr, isV2 } = getTokenFields(credentialData.plainDataObj) + const isExpired = expiryStr ? new Date(expiryStr) < new Date() : false + setIsV2Credential(isV2) setIsTokenExpired(isExpired) - setAccessToken(credentialData.plainDataObj.googleAccessToken ?? '') + setAccessToken(token) } }, [credentialData]) // Handle credential data from API useEffect(() => { if (getCredentialDataApi.data) { - const expiresAt = new Date(getCredentialDataApi.data?.plainDataObj.expiresAt) - const now = new Date() - const isExpired = expiresAt < now - + const { token, expiryStr, isV2 } = getTokenFields(getCredentialDataApi.data?.plainDataObj) + const isExpired = expiryStr ? new Date(expiryStr) < new Date() : false + setIsV2Credential(isV2) setIsTokenExpired(isExpired) - setAccessToken(getCredentialDataApi.data?.plainDataObj.googleAccessToken ?? '') + setAccessToken(token) handleCredentialDataChange?.(getCredentialDataApi.data) } }, [getCredentialDataApi.data, handleCredentialDataChange]) @@ -301,35 +312,70 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre try { setIsRefreshing(true) - // console.log('🔄 [FRONTEND] Iniciando refresh para credential:', credentialId) - // Obtener token actual antes del refresh - // const currentCred = await credentialsApi.getSpecificCredential(credentialId) - // const oldToken = currentCred?.data?.plainDataObj?.googleAccessToken?.substring(0, 20) - // console.log('🔄 [FRONTEND] Token actual:', oldToken + '...') + if (isV2Credential) { + // googleDriveOAuth2 (v2) — use the generic OAuth2 refresh endpoint + await oauth2Api.refresh(credentialId) + } else { + // googleOAuth (v1) — use the legacy refresh endpoint + await credentialsApi.refreshAccessToken({ credentialId }) + } - const response = await credentialsApi.refreshAccessToken({ credentialId }) + getCredentialDataApi.request(credentialId) + setIsTokenExpired(false) + setIsReauthRequired(false) + showSnackbar('Successfully refreshed access token', 'success') + } catch (error) { + const status = error.response?.status + const errorMessage = error.response?.data?.message || error.message || 'Error refreshing access token' + + if (status === 401 || errorMessage.includes('REAUTH_REQUIRED') || errorMessage.includes('re-authenticate')) { + // Refresh token is revoked — full re-auth needed via OAuth popup + setIsReauthRequired(true) + showSnackbar('Re-authentication required. Please click "Re-authenticate with Google".', 'warning') + } else { + showSnackbar(errorMessage, 'error') + } + } finally { + setIsRefreshing(false) + } + }, [credentialId, isV2Credential, getCredentialDataApi, showSnackbar]) - if (response.status === 200) { - getCredentialDataApi.request(credentialId) + const handleReauthenticate = useCallback(async () => { + if (!credentialId) return - // Verificar que el token cambió - setTimeout(async () => { - // const newCred = await credentialsApi.getSpecificCredential(credentialId) - // const newToken = newCred?.data?.plainDataObj?.googleAccessToken?.substring(0, 20) - // console.log('✅ [FRONTEND] Token nuevo:', newToken + '...') - // console.log('🔍 [FRONTEND] ¿Token cambió?', oldToken !== newToken) - }, 1000) + try { + const authResponse = await oauth2Api.authorize(credentialId) + + if (authResponse.data?.authorizationUrl) { + const authWindow = window.open( + authResponse.data.authorizationUrl, + '_blank', + 'width=600,height=700,scrollbars=yes,resizable=yes' + ) + + if (!authWindow) { + showSnackbar('Popup blocked. Please allow popups for this site and try again.', 'error') + return + } + + const handleMessage = (event) => { + if (event.data?.type === 'OAUTH2_SUCCESS') { + window.removeEventListener('message', handleMessage) + getCredentialDataApi.request(credentialId) + setIsTokenExpired(false) + setIsReauthRequired(false) + showSnackbar('Successfully re-authenticated with Google', 'success') + } else if (event.data?.type === 'OAUTH2_ERROR') { + window.removeEventListener('message', handleMessage) + showSnackbar(event.data.error || 'Re-authentication failed', 'error') + } + } - setIsTokenExpired(false) - showSnackbar('Successfully refreshed access token', 'success') + window.addEventListener('message', handleMessage) } } catch (error) { - console.error('❌ [FRONTEND] Error refreshing access token:', error) - const errorMessage = error.response?.data?.message || 'Error refreshing access token' - showSnackbar(errorMessage, 'error') - } finally { - setIsRefreshing(false) + showSnackbar(error.response?.data?.message || 'Failed to start re-authentication', 'error') } }, [credentialId, getCredentialDataApi, showSnackbar]) @@ -359,7 +405,7 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre )} - {isTokenExpired && ( + {isTokenExpired && !isReauthRequired && ( + )} - {isTokenExpired && ( + {isTokenExpired && !isReauthRequired && ( - Access token has expired. Please re-authenticate or refresh the access token. + Access token has expired. Click "Refresh Access Token" to renew it. + + )} + + {isReauthRequired && ( + + Google authorization has expired or been revoked. Click "Re-authenticate with Google" to reconnect. )} From d9d5593f968fd1154d138a96e0a4e960236a83ef Mon Sep 17 00:00:00 2001 From: Max Techera Date: Thu, 19 Feb 2026 17:10:54 -0300 Subject: [PATCH 2/2] refactor(SUPPORT-19): migrate Google Drive loader to googleDriveOAuth2 only - Remove googleOAuth (v1) from GoogleDrive node credentialNames - Drop v1 credential field fallbacks (googleAccessToken, expiresAt) - Simplify GoogleDrivePicker to use v2 fields (access_token, expires_at) directly - Refresh always uses /oauth2-credential/refresh/:id endpoint --- .../GoogleDrive/GoogleDrive.ts | 4 +- .../ui-component/drive/GoogleDrivePicker.jsx | 37 ++++--------------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts b/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts index e7fdb4e6efc..603c2be09cf 100644 --- a/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts +++ b/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts @@ -60,7 +60,7 @@ class GoogleDrive_DocumentLoaders implements INode { name: 'credential', type: 'credential', description: 'Google Drive OAuth2 Credential', - credentialNames: ['googleDriveOAuth2', 'googleOAuth'] + credentialNames: ['googleDriveOAuth2'] } this.inputs = [ { @@ -205,7 +205,7 @@ class GoogleDrive_DocumentLoaders implements INode { try { let credentialData = await getCredentialData(nodeData.credential ?? '', options) credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) - const accessToken = getCredentialParam('access_token', credentialData, nodeData) || credentialData.googleAccessToken + const accessToken = getCredentialParam('access_token', credentialData, nodeData) if (!accessToken) { return returnData diff --git a/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx b/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx index f0e718e9d90..83ad31577d7 100644 --- a/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx +++ b/packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx @@ -8,14 +8,6 @@ import { IconX, IconTrash } from '@tabler/icons-react' import { useDispatch } from 'react-redux' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' -// Extract token fields from credential data — handles both googleOAuth (v1) and googleDriveOAuth2 (v2) -const getTokenFields = (plainDataObj) => { - const token = plainDataObj.googleAccessToken || plainDataObj.access_token || '' - const expiryStr = plainDataObj.expiresAt || plainDataObj.expires_at - const isV2 = !!plainDataObj.access_token && !plainDataObj.googleAccessToken - return { token, expiryStr, isV2 } -} - export const SUPPORTED_MIME_TYPES = [ 'application/vnd.google-apps.document', 'application/vnd.google-apps.spreadsheet', @@ -185,7 +177,6 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre const [accessToken, setAccessToken] = useState(null) const [isTokenExpired, setIsTokenExpired] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) - const [isV2Credential, setIsV2Credential] = useState(false) const [isReauthRequired, setIsReauthRequired] = useState(false) const enqueueSnackbar = useCallback((...args) => dispatch(enqueueSnackbarAction(...args)), [dispatch]) @@ -221,7 +212,6 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre // Reset token state setAccessToken(null) setIsTokenExpired(false) - setIsV2Credential(false) setIsReauthRequired(false) } }, [credentialId]) @@ -229,22 +219,20 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre // Handle credential data from prop useEffect(() => { if (credentialData?.plainDataObj) { - const { token, expiryStr, isV2 } = getTokenFields(credentialData.plainDataObj) - const isExpired = expiryStr ? new Date(expiryStr) < new Date() : false - setIsV2Credential(isV2) + const { access_token, expires_at } = credentialData.plainDataObj + const isExpired = expires_at ? new Date(expires_at) < new Date() : false setIsTokenExpired(isExpired) - setAccessToken(token) + setAccessToken(access_token ?? '') } }, [credentialData]) // Handle credential data from API useEffect(() => { if (getCredentialDataApi.data) { - const { token, expiryStr, isV2 } = getTokenFields(getCredentialDataApi.data?.plainDataObj) - const isExpired = expiryStr ? new Date(expiryStr) < new Date() : false - setIsV2Credential(isV2) + const { access_token, expires_at } = getCredentialDataApi.data?.plainDataObj + const isExpired = expires_at ? new Date(expires_at) < new Date() : false setIsTokenExpired(isExpired) - setAccessToken(token) + setAccessToken(access_token ?? '') handleCredentialDataChange?.(getCredentialDataApi.data) } }, [getCredentialDataApi.data, handleCredentialDataChange]) @@ -312,15 +300,7 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre try { setIsRefreshing(true) - - if (isV2Credential) { - // googleDriveOAuth2 (v2) — use the generic OAuth2 refresh endpoint - await oauth2Api.refresh(credentialId) - } else { - // googleOAuth (v1) — use the legacy refresh endpoint - await credentialsApi.refreshAccessToken({ credentialId }) - } - + await oauth2Api.refresh(credentialId) getCredentialDataApi.request(credentialId) setIsTokenExpired(false) setIsReauthRequired(false) @@ -330,7 +310,6 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre const errorMessage = error.response?.data?.message || error.message || 'Error refreshing access token' if (status === 401 || errorMessage.includes('REAUTH_REQUIRED') || errorMessage.includes('re-authenticate')) { - // Refresh token is revoked — full re-auth needed via OAuth popup setIsReauthRequired(true) showSnackbar('Re-authentication required. Please click "Re-authenticate with Google".', 'warning') } else { @@ -339,7 +318,7 @@ export const GoogleDrivePicker = ({ onChange, value, disabled, credentialId, cre } finally { setIsRefreshing(false) } - }, [credentialId, isV2Credential, getCredentialDataApi, showSnackbar]) + }, [credentialId, getCredentialDataApi, showSnackbar]) const handleReauthenticate = useCallback(async () => { if (!credentialId) return