From f33d3b224d1801dc298a050b2d4d6a589f0a09ba Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 22 Jul 2025 10:58:37 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Fix=20Google=20Sheet=20?= =?UTF-8?q?authentication=20not=20refreshing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/builder/package.json | 1 - .../googleSheets/api/getSpreadsheetName.ts | 26 +++-- .../google-sheets/spreadsheets/[id]/sheets.ts | 15 ++- .../[spreadsheetId]/sheets/[sheetId].ts | 32 +++--- bun.lock | 25 +++-- .../integrations/googleSheets/getRow.ts | 16 ++- .../helpers/getAuthenticatedGoogleDoc.ts | 31 ------ .../integrations/googleSheets/insertRow.ts | 16 ++- .../integrations/googleSheets/updateRow.ts | 16 ++- packages/credentials/package.json | 2 +- .../src/decryptAndRefreshCredentials.ts | 2 +- .../src/getAuthenticatedGoogleClient.ts | 45 --------- .../credentials/src/getGoogleSpreadsheet.ts | 98 +++++++++++++++++++ 13 files changed, 199 insertions(+), 126 deletions(-) delete mode 100644 packages/bot-engine/src/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts delete mode 100644 packages/credentials/src/getAuthenticatedGoogleClient.ts create mode 100644 packages/credentials/src/getGoogleSpreadsheet.ts diff --git a/apps/builder/package.json b/apps/builder/package.json index 59deb3a4ac..da792a1ed8 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -79,7 +79,6 @@ "emojilib": "3.0.10", "framer-motion": "11.1.7", "google-auth-library": "10.1.0", - "google-spreadsheet": "4.1.4", "immer": "10.0.2", "ioredis": "5.4.1", "jsonwebtoken": "9.0.1", diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/api/getSpreadsheetName.ts b/apps/builder/src/features/blocks/integrations/googleSheets/api/getSpreadsheetName.ts index e349d9b07b..f85fcdbbd0 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/api/getSpreadsheetName.ts +++ b/apps/builder/src/features/blocks/integrations/googleSheets/api/getSpreadsheetName.ts @@ -1,10 +1,9 @@ import { isReadWorkspaceFobidden } from "@/features/workspace/helpers/isReadWorkspaceFobidden"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; -import { getAuthenticatedGoogleClient } from "@typebot.io/credentials/getAuthenticatedGoogleClient"; +import { getGoogleSpreadsheet } from "@typebot.io/credentials/getGoogleSpreadsheet"; import prisma from "@typebot.io/prisma"; import { z } from "@typebot.io/zod"; -import { GoogleSpreadsheet } from "google-spreadsheet"; export const getSpreadsheetName = authenticatedProcedure .input( @@ -51,22 +50,19 @@ export const getSpreadsheetName = authenticatedProcedure message: "Credentials not found", }); - const client = await getAuthenticatedGoogleClient( - credentials.id, - workspaceId, - ); - - if (!client?.credentials.access_token) - throw new TRPCError({ - code: "NOT_FOUND", - message: "Google client could not be initialized", - }); - try { - const googleSheet = new GoogleSpreadsheet(spreadsheetId, { - token: client.credentials.access_token, + const googleSheet = await getGoogleSpreadsheet({ + credentialsId: credentials.id, + spreadsheetId, + workspaceId, }); + if (!googleSheet) + throw new TRPCError({ + code: "NOT_FOUND", + message: "Google sheet not found", + }); + await googleSheet.loadInfo(); return { name: googleSheet.title }; diff --git a/apps/builder/src/pages/api/integrations/google-sheets/spreadsheets/[id]/sheets.ts b/apps/builder/src/pages/api/integrations/google-sheets/spreadsheets/[id]/sheets.ts index 671e67d708..18ba1326a1 100644 --- a/apps/builder/src/pages/api/integrations/google-sheets/spreadsheets/[id]/sheets.ts +++ b/apps/builder/src/pages/api/integrations/google-sheets/spreadsheets/[id]/sheets.ts @@ -1,12 +1,11 @@ import { getAuthenticatedUser } from "@/features/auth/helpers/getAuthenticatedUser"; -import { getAuthenticatedGoogleClient } from "@typebot.io/credentials/getAuthenticatedGoogleClient"; +import { getGoogleSpreadsheet } from "@typebot.io/credentials/getGoogleSpreadsheet"; import { badRequest, methodNotAllowed, notAuthenticated, } from "@typebot.io/lib/api/utils"; import { isDefined } from "@typebot.io/lib/utils"; -import { GoogleSpreadsheet } from "google-spreadsheet"; import type { NextApiRequest, NextApiResponse } from "next"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { @@ -18,17 +17,16 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const workspaceId = req.query.workspaceId as string | undefined; if (!credentialsId || workspaceId) return badRequest(res); const spreadsheetId = req.query.id as string; - const client = await getAuthenticatedGoogleClient( + const doc = await getGoogleSpreadsheet({ credentialsId, + spreadsheetId, workspaceId, - ); - if (!client?.credentials.access_token) + }); + if (!doc) return res .status(404) .send({ message: "Couldn't find credentials in database" }); - const doc = new GoogleSpreadsheet(spreadsheetId, { - token: client.credentials.access_token, - }); + try { await doc.loadInfo(); return res.send({ @@ -53,6 +51,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { ).filter(isDefined), }); } catch (err) { + console.log(err); return res.status(404).send({ message: "Couldn't find sheet, you maybe don't have permission to read it", diff --git a/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts b/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts index 4a4e02326a..3bc8b624d9 100644 --- a/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts +++ b/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts @@ -4,12 +4,11 @@ import type { GoogleSheetsInsertRowOptions, GoogleSheetsUpdateRowOptions, } from "@typebot.io/blocks-integrations/googleSheets/schema"; -import { getAuthenticatedGoogleDoc } from "@typebot.io/bot-engine/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc"; import { saveErrorLog } from "@typebot.io/bot-engine/logs/saveErrorLog"; import { saveSuccessLog } from "@typebot.io/bot-engine/logs/saveSuccessLog"; import { LogicalOperator } from "@typebot.io/conditions/constants"; import { ComparisonOperators } from "@typebot.io/conditions/constants"; -import { getAuthenticatedGoogleClient } from "@typebot.io/credentials/getAuthenticatedGoogleClient"; +import { getGoogleSpreadsheet } from "@typebot.io/credentials/getGoogleSpreadsheet"; import { badRequest, initMiddleware, @@ -18,10 +17,7 @@ import { } from "@typebot.io/lib/api/utils"; import { hasValue, isDefined } from "@typebot.io/lib/utils"; import Cors from "cors"; -import { - GoogleSpreadsheet, - type GoogleSpreadsheetRow, -} from "google-spreadsheet"; +import type { GoogleSpreadsheetRow } from "google-spreadsheet"; import type { NextApiRequest, NextApiResponse } from "next"; const cors = initMiddleware(Cors()); @@ -67,11 +63,15 @@ const getRows = async (req: NextApiRequest, res: NextApiResponse) => { return; } - const doc = await getAuthenticatedGoogleDoc({ + const doc = await getGoogleSpreadsheet({ credentialsId, spreadsheetId, - workspaceId: "", + workspaceId: undefined, }); + if (!doc) { + notFound(res); + return; + } await doc.loadInfo(); const sheet = doc.sheetsById[Number(sheetId)]; try { @@ -123,11 +123,15 @@ const insertRow = async (req: NextApiRequest, res: NextApiResponse) => { values: { [key: string]: string }; }; if (!hasValue(credentialsId)) return badRequest(res); - const doc = await getAuthenticatedGoogleDoc({ + const doc = await getGoogleSpreadsheet({ credentialsId, spreadsheetId, - workspaceId: "", + workspaceId: undefined, }); + if (!doc) { + notFound(res); + return; + } try { await doc.loadInfo(); const sheet = doc.sheetsById[Number(sheetId)]; @@ -156,11 +160,15 @@ const updateRow = async (req: NextApiRequest, res: NextApiResponse) => { const { resultId, credentialsId, values } = body; if (!hasValue(credentialsId) || !referenceCell) return badRequest(res); - const doc = await getAuthenticatedGoogleDoc({ + const doc = await getGoogleSpreadsheet({ credentialsId, spreadsheetId, - workspaceId: "", + workspaceId: undefined, }); + if (!doc) { + notFound(res); + return; + } try { await doc.loadInfo(); const sheet = doc.sheetsById[Number(sheetId)]; diff --git a/bun.lock b/bun.lock index a7b749f16b..b719ca53d0 100644 --- a/bun.lock +++ b/bun.lock @@ -84,7 +84,6 @@ "emojilib": "3.0.10", "framer-motion": "11.1.7", "google-auth-library": "10.1.0", - "google-spreadsheet": "4.1.4", "immer": "10.0.2", "ioredis": "5.4.1", "jsonwebtoken": "9.0.1", @@ -509,7 +508,7 @@ "@typebot.io/lib": "workspace:*", "@typebot.io/prisma": "workspace:*", "@typebot.io/zod": "workspace:*", - "google-auth-library": "10.1.0", + "google-spreadsheet": "4.1.4", "ky": "1.2.4", "qs": "6.11.2", }, @@ -5467,7 +5466,7 @@ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], @@ -6791,8 +6790,6 @@ "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], - "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "body-parser/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -6841,6 +6838,8 @@ "compress-commons/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "content-disposition/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "crc32-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "create-emotion/@emotion/cache": ["@emotion/cache@10.0.29", "", { "dependencies": { "@emotion/sheet": "0.9.4", "@emotion/stylis": "0.8.5", "@emotion/utils": "0.11.3", "@emotion/weak-memoize": "0.2.5" } }, "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ=="], @@ -6863,6 +6862,8 @@ "dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="], @@ -6905,6 +6906,8 @@ "express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "express/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "express/setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "express/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -7107,6 +7110,10 @@ "juice/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="], + "jwa/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "jws/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "lambda-local/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], @@ -7349,6 +7356,8 @@ "public-ip/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + "randombytes/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "react-email/@babel/core": ["@babel/core@7.24.5", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.24.5", "@babel/helpers": "^7.24.5", "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", "@babel/traverse": "^7.24.5", "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA=="], "react-email/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -7493,6 +7502,8 @@ "stdin-discarder/bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="], + "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "style-to-js/style-to-object": ["style-to-object@1.0.9", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="], @@ -7527,6 +7538,8 @@ "tsup/esbuild": ["esbuild@0.25.6", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.6", "@esbuild/android-arm": "0.25.6", "@esbuild/android-arm64": "0.25.6", "@esbuild/android-x64": "0.25.6", "@esbuild/darwin-arm64": "0.25.6", "@esbuild/darwin-x64": "0.25.6", "@esbuild/freebsd-arm64": "0.25.6", "@esbuild/freebsd-x64": "0.25.6", "@esbuild/linux-arm": "0.25.6", "@esbuild/linux-arm64": "0.25.6", "@esbuild/linux-ia32": "0.25.6", "@esbuild/linux-loong64": "0.25.6", "@esbuild/linux-mips64el": "0.25.6", "@esbuild/linux-ppc64": "0.25.6", "@esbuild/linux-riscv64": "0.25.6", "@esbuild/linux-s390x": "0.25.6", "@esbuild/linux-x64": "0.25.6", "@esbuild/netbsd-arm64": "0.25.6", "@esbuild/netbsd-x64": "0.25.6", "@esbuild/openbsd-arm64": "0.25.6", "@esbuild/openbsd-x64": "0.25.6", "@esbuild/openharmony-arm64": "0.25.6", "@esbuild/sunos-x64": "0.25.6", "@esbuild/win32-arm64": "0.25.6", "@esbuild/win32-ia32": "0.25.6", "@esbuild/win32-x64": "0.25.6" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg=="], + "tunnel-agent/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "unbzip2-stream/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "unctx/unplugin": ["unplugin@2.3.5", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw=="], @@ -8435,7 +8448,7 @@ "jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], - "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "jsonwebtoken/jws/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], diff --git a/packages/bot-engine/src/blocks/integrations/googleSheets/getRow.ts b/packages/bot-engine/src/blocks/integrations/googleSheets/getRow.ts index 060697fadf..0944fbc3cf 100644 --- a/packages/bot-engine/src/blocks/integrations/googleSheets/getRow.ts +++ b/packages/bot-engine/src/blocks/integrations/googleSheets/getRow.ts @@ -1,5 +1,6 @@ import type { GoogleSheetsGetOptions } from "@typebot.io/blocks-integrations/googleSheets/schema"; import type { SessionState } from "@typebot.io/chat-session/schemas"; +import { getGoogleSpreadsheet } from "@typebot.io/credentials/getGoogleSpreadsheet"; import { parseUnknownError } from "@typebot.io/lib/parseUnknownError"; import { byId, isDefined, isNotEmpty } from "@typebot.io/lib/utils"; import type { LogInSession } from "@typebot.io/logs/schemas"; @@ -8,7 +9,6 @@ import { deepParseVariables } from "@typebot.io/variables/deepParseVariables"; import type { VariableWithValue } from "@typebot.io/variables/schemas"; import type { ExecuteIntegrationResponse } from "../../../types"; import { updateVariablesInSession } from "../../../updateVariablesInSession"; -import { getAuthenticatedGoogleDoc } from "./helpers/getAuthenticatedGoogleDoc"; import { matchFilter } from "./helpers/matchFilter"; export const getRow = async ( @@ -45,12 +45,24 @@ export const getRow = async ( ], }; - const doc = await getAuthenticatedGoogleDoc({ + const doc = await getGoogleSpreadsheet({ credentialsId: options.credentialsId, spreadsheetId: options.spreadsheetId, workspaceId: state.workspaceId, }); + if (!doc) + return { + outgoingEdgeId, + logs: [ + { + status: "error", + description: "Couldn't find credentials in database", + context: "While getting spreadsheet row", + }, + ], + }; + try { await doc.loadInfo(); const sheet = doc.sheetsById[Number(sheetId)]; diff --git a/packages/bot-engine/src/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts b/packages/bot-engine/src/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts deleted file mode 100644 index da98c56306..0000000000 --- a/packages/bot-engine/src/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TRPCError } from "@trpc/server"; -import { getAuthenticatedGoogleClient } from "@typebot.io/credentials/getAuthenticatedGoogleClient"; -import { GoogleSpreadsheet } from "google-spreadsheet"; - -export const getAuthenticatedGoogleDoc = async ({ - credentialsId, - spreadsheetId, - workspaceId, -}: { - credentialsId: string; - spreadsheetId: string; - workspaceId: string | undefined; -}) => { - const client = await getAuthenticatedGoogleClient(credentialsId, workspaceId); - if (!client) - throw new TRPCError({ - code: "NOT_FOUND", - message: "Couldn't find credentials in database", - }); - - const accessToken = client.credentials.access_token; - if (!accessToken) - throw new TRPCError({ - code: "NOT_FOUND", - message: "No access token found in credentials", - }); - - return new GoogleSpreadsheet(spreadsheetId, { - token: accessToken, - }); -}; diff --git a/packages/bot-engine/src/blocks/integrations/googleSheets/insertRow.ts b/packages/bot-engine/src/blocks/integrations/googleSheets/insertRow.ts index 3858e4f249..4e99f5efdf 100644 --- a/packages/bot-engine/src/blocks/integrations/googleSheets/insertRow.ts +++ b/packages/bot-engine/src/blocks/integrations/googleSheets/insertRow.ts @@ -1,10 +1,10 @@ import type { GoogleSheetsInsertRowOptions } from "@typebot.io/blocks-integrations/googleSheets/schema"; import type { SessionState } from "@typebot.io/chat-session/schemas"; +import { getGoogleSpreadsheet } from "@typebot.io/credentials/getGoogleSpreadsheet"; import { parseUnknownError } from "@typebot.io/lib/parseUnknownError"; import type { LogInSession } from "@typebot.io/logs/schemas"; import type { SessionStore } from "@typebot.io/runtime-session-store"; import type { ExecuteIntegrationResponse } from "../../../types"; -import { getAuthenticatedGoogleDoc } from "./helpers/getAuthenticatedGoogleDoc"; import { parseNewRowObject } from "./helpers/parseNewRowObject"; export const insertRow = async ( @@ -34,12 +34,24 @@ export const insertRow = async ( const logs: LogInSession[] = []; - const doc = await getAuthenticatedGoogleDoc({ + const doc = await getGoogleSpreadsheet({ credentialsId: options.credentialsId, spreadsheetId: options.spreadsheetId, workspaceId: state.workspaceId, }); + if (!doc) + return { + outgoingEdgeId, + logs: [ + { + status: "error", + description: "Couldn't find credentials in database", + context: "While inserting row in spreadsheet", + }, + ], + }; + const parsedValues = parseNewRowObject(options.cellsToInsert, { variables, sessionStore, diff --git a/packages/bot-engine/src/blocks/integrations/googleSheets/updateRow.ts b/packages/bot-engine/src/blocks/integrations/googleSheets/updateRow.ts index 756d2cb970..3824ef18db 100644 --- a/packages/bot-engine/src/blocks/integrations/googleSheets/updateRow.ts +++ b/packages/bot-engine/src/blocks/integrations/googleSheets/updateRow.ts @@ -1,11 +1,11 @@ import type { GoogleSheetsUpdateRowOptions } from "@typebot.io/blocks-integrations/googleSheets/schema"; import type { SessionState } from "@typebot.io/chat-session/schemas"; +import { getGoogleSpreadsheet } from "@typebot.io/credentials/getGoogleSpreadsheet"; import { parseUnknownError } from "@typebot.io/lib/parseUnknownError"; import type { LogInSession } from "@typebot.io/logs/schemas"; import type { SessionStore } from "@typebot.io/runtime-session-store"; import { deepParseVariables } from "@typebot.io/variables/deepParseVariables"; import type { ExecuteIntegrationResponse } from "../../../types"; -import { getAuthenticatedGoogleDoc } from "./helpers/getAuthenticatedGoogleDoc"; import { matchFilter } from "./helpers/matchFilter"; import { parseNewCellValuesObject } from "./helpers/parseNewCellValuesObject"; @@ -48,12 +48,24 @@ export const updateRow = async ( const logs: LogInSession[] = []; - const doc = await getAuthenticatedGoogleDoc({ + const doc = await getGoogleSpreadsheet({ credentialsId: options.credentialsId, spreadsheetId: options.spreadsheetId, workspaceId: state.workspaceId, }); + if (!doc) + return { + outgoingEdgeId, + logs: [ + { + status: "error", + description: "Couldn't find credentials in database", + context: "While updating row in spreadsheet", + }, + ], + }; + try { await doc.loadInfo(); const sheet = doc.sheetsById[Number(sheetId)]; diff --git a/packages/credentials/package.json b/packages/credentials/package.json index 8f879971e5..f60ae0747e 100644 --- a/packages/credentials/package.json +++ b/packages/credentials/package.json @@ -8,7 +8,7 @@ "dependencies": { "@typebot.io/zod": "workspace:*", "@typebot.io/forge-repository": "workspace:*", - "google-auth-library": "10.1.0", + "google-spreadsheet": "4.1.4", "@typebot.io/lib": "workspace:*", "@typebot.io/env": "workspace:*", "@typebot.io/prisma": "workspace:*", diff --git a/packages/credentials/src/decryptAndRefreshCredentials.ts b/packages/credentials/src/decryptAndRefreshCredentials.ts index 4bfb183b6e..1a03d82d97 100644 --- a/packages/credentials/src/decryptAndRefreshCredentials.ts +++ b/packages/credentials/src/decryptAndRefreshCredentials.ts @@ -39,7 +39,7 @@ export const decryptAndRefreshCredentialsData = async ( if (!client) throw new Error("No client found for oauth block"); - if (!expiryDate || expiryDate > Date.now()) + if (expiryDate && expiryDate > Date.now()) return { ...decryptedData, client, diff --git a/packages/credentials/src/getAuthenticatedGoogleClient.ts b/packages/credentials/src/getAuthenticatedGoogleClient.ts deleted file mode 100644 index c2ff598df9..0000000000 --- a/packages/credentials/src/getAuthenticatedGoogleClient.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { env } from "@typebot.io/env"; -import { isDefined } from "@typebot.io/lib/utils"; -import prisma from "@typebot.io/prisma"; -import { type Credentials, OAuth2Client } from "google-auth-library"; -import { decrypt } from "./decrypt"; -import { encrypt } from "./encrypt"; -import { getCredentials } from "./getCredentials"; - -export const getAuthenticatedGoogleClient = async ( - credentialsId: string, - workspaceId: string | undefined, -): Promise => { - const credentials = await getCredentials(credentialsId, workspaceId); - if (!credentials) return; - const data = await decrypt(credentials.data, credentials.iv); - - const oauth2Client = new OAuth2Client( - env.GOOGLE_SHEETS_CLIENT_ID, - env.GOOGLE_SHEETS_CLIENT_SECRET, - `${env.NEXTAUTH_URL}/api/credentials/google-sheets/callback`, - ); - oauth2Client.setCredentials(data); - oauth2Client.on("tokens", updateTokens(credentialsId, data)); - return oauth2Client; -}; - -const updateTokens = - (credentialsId: string, existingCredentials: any) => - async (credentials: Credentials) => { - if ( - isDefined(existingCredentials.id_token) && - credentials.id_token !== existingCredentials.id_token - ) - return; - const newCredentials = { - ...existingCredentials, - expiry_date: credentials.expiry_date, - access_token: credentials.access_token, - }; - const { encryptedData, iv } = await encrypt(newCredentials); - await prisma.credentials.updateMany({ - where: { id: credentialsId }, - data: { data: encryptedData, iv }, - }); - }; diff --git a/packages/credentials/src/getGoogleSpreadsheet.ts b/packages/credentials/src/getGoogleSpreadsheet.ts new file mode 100644 index 0000000000..89868238cb --- /dev/null +++ b/packages/credentials/src/getGoogleSpreadsheet.ts @@ -0,0 +1,98 @@ +import { env } from "@typebot.io/env"; +import { parseUnknownError } from "@typebot.io/lib/parseUnknownError"; +import prisma from "@typebot.io/prisma"; +import { GoogleSpreadsheet } from "google-spreadsheet"; +import ky from "ky"; +import { decrypt } from "./decrypt"; +import { encrypt } from "./encrypt"; +import { getCredentials } from "./getCredentials"; +import type { GoogleSheetsCredentials } from "./schemas"; + +const TOKEN_URL = "https://oauth2.googleapis.com/token" as const; + +type Params = { + spreadsheetId: string; + credentialsId: string; + workspaceId: string | undefined; +}; + +export const getGoogleSpreadsheet = async ({ + spreadsheetId, + credentialsId, + workspaceId, +}: Params): Promise => { + const credentials = await getCredentials(credentialsId, workspaceId); + if (!credentials) return; + const decryptedData = await decrypt(credentials.data, credentials.iv); + const { refresh_token, expiry_date, access_token } = + decryptedData as GoogleSheetsCredentials["data"]; + + if (!access_token) + throw new Error("No access token found in Sheets credentials"); + + const client = { + id: env.GOOGLE_SHEETS_CLIENT_ID, + secret: env.GOOGLE_SHEETS_CLIENT_SECRET, + }; + + console.log("is expired", expiry_date && expiry_date > Date.now()); + + if (expiry_date && expiry_date > Date.now()) + return new GoogleSpreadsheet(spreadsheetId, { + token: access_token, + }); + + try { + if (!refresh_token) + throw new Error("No refresh token found in Sheets credentials"); + + const tokens = await ky + .post(TOKEN_URL, { + json: { + grant_type: "refresh_token", + refresh_token: refresh_token, + client_id: client.id, + client_secret: client.secret, + redirect_uri: `${env.NEXTAUTH_URL}/oauth/redirect`, + }, + }) + .json(); + + if ( + !tokens || + typeof tokens !== "object" || + !("access_token" in tokens) || + !("expires_in" in tokens) || + typeof tokens.access_token !== "string" || + typeof tokens.expires_in !== "number" + ) + throw new Error("Invalid tokens returned from the auth provider"); + + const newTokens = { + ...decryptedData, + access_token: tokens.access_token, + expiry_date: Date.now() + (tokens.expires_in ?? 3600) * 1000, // Default 1 hour + } satisfies GoogleSheetsCredentials["data"]; + + const { encryptedData, iv } = await encrypt(newTokens); + + await prisma.credentials.update({ + where: { id: credentialsId }, + data: { + data: encryptedData, + iv, + }, + }); + + return new GoogleSpreadsheet(spreadsheetId, { + token: newTokens.access_token, + }); + } catch (err) { + const parsedError = await parseUnknownError({ + err, + context: "token exchange", + }); + console.error(parsedError); + throw new Error(parsedError.description); + } +};