-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
⚡ (sheets) Use Google Drive picker and remove sensitive OAuth scope
BREAKING CHANGE: The Google Picker API needs to be enabled in the Google Cloud console. You also need to enable it in your NEXT_PUBLIC_GOOGLE_API_KEY. You also need to add the drive.file OAuth scope.
- Loading branch information
1 parent
2dec0b8
commit deab1a1
Showing
23 changed files
with
428 additions
and
156 deletions.
There are no files selected for viewing
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions
60
apps/builder/src/features/blocks/integrations/googleSheets/api/getAccessToken.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import prisma from '@typebot.io/lib/prisma' | ||
import { authenticatedProcedure } from '@/helpers/server/trpc' | ||
import { TRPCError } from '@trpc/server' | ||
import { z } from 'zod' | ||
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden' | ||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' | ||
import { GoogleSheetsCredentials } from '@typebot.io/schemas' | ||
import { OAuth2Client } from 'google-auth-library' | ||
import { env } from '@typebot.io/env' | ||
|
||
export const getAccessToken = authenticatedProcedure | ||
.input( | ||
z.object({ | ||
workspaceId: z.string(), | ||
credentialsId: z.string(), | ||
}) | ||
) | ||
.query(async ({ input: { workspaceId, credentialsId }, ctx: { user } }) => { | ||
const workspace = await prisma.workspace.findFirst({ | ||
where: { | ||
id: workspaceId, | ||
}, | ||
select: { | ||
id: true, | ||
members: true, | ||
credentials: { | ||
where: { | ||
id: credentialsId, | ||
}, | ||
select: { | ||
data: true, | ||
iv: true, | ||
}, | ||
}, | ||
}, | ||
}) | ||
if (!workspace || isReadWorkspaceFobidden(workspace, user)) | ||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' }) | ||
|
||
const credentials = workspace.credentials[0] | ||
if (!credentials) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Credentials not found', | ||
}) | ||
const decryptedCredentials = (await decrypt( | ||
credentials.data, | ||
credentials.iv | ||
)) as GoogleSheetsCredentials['data'] | ||
|
||
const client = new OAuth2Client({ | ||
clientId: env.GOOGLE_CLIENT_ID, | ||
clientSecret: env.GOOGLE_CLIENT_SECRET, | ||
redirectUri: `${env.NEXTAUTH_URL}/api/credentials/google-sheets/callback`, | ||
}) | ||
|
||
client.setCredentials(decryptedCredentials) | ||
|
||
return { accessToken: (await client.getAccessToken()).token } | ||
}) |
72 changes: 72 additions & 0 deletions
72
apps/builder/src/features/blocks/integrations/googleSheets/api/getSpreadsheetName.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import prisma from '@typebot.io/lib/prisma' | ||
import { authenticatedProcedure } from '@/helpers/server/trpc' | ||
import { TRPCError } from '@trpc/server' | ||
import { z } from 'zod' | ||
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden' | ||
import { getAuthenticatedGoogleClient } from '@typebot.io/lib/google' | ||
import { GoogleSpreadsheet } from 'google-spreadsheet' | ||
|
||
export const getSpreadsheetName = authenticatedProcedure | ||
.input( | ||
z.object({ | ||
workspaceId: z.string(), | ||
credentialsId: z.string(), | ||
spreadsheetId: z.string(), | ||
}) | ||
) | ||
.query( | ||
async ({ | ||
input: { workspaceId, credentialsId, spreadsheetId }, | ||
ctx: { user }, | ||
}) => { | ||
const workspace = await prisma.workspace.findFirst({ | ||
where: { | ||
id: workspaceId, | ||
}, | ||
select: { | ||
id: true, | ||
members: true, | ||
credentials: { | ||
where: { | ||
id: credentialsId, | ||
}, | ||
select: { | ||
id: true, | ||
data: true, | ||
iv: true, | ||
}, | ||
}, | ||
}, | ||
}) | ||
if (!workspace || isReadWorkspaceFobidden(workspace, user)) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Workspace not found', | ||
}) | ||
|
||
const credentials = workspace.credentials[0] | ||
if (!credentials) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Credentials not found', | ||
}) | ||
|
||
const client = await getAuthenticatedGoogleClient(credentials.id) | ||
|
||
if (!client) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Google client could not be initialized', | ||
}) | ||
|
||
try { | ||
const googleSheet = new GoogleSpreadsheet(spreadsheetId, client) | ||
|
||
await googleSheet.loadInfo() | ||
|
||
return { name: googleSheet.title } | ||
} catch (e) { | ||
return { name: '' } | ||
} | ||
} | ||
) |
8 changes: 8 additions & 0 deletions
8
apps/builder/src/features/blocks/integrations/googleSheets/api/router.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { router } from '@/helpers/server/trpc' | ||
import { getAccessToken } from './getAccessToken' | ||
import { getSpreadsheetName } from './getSpreadsheetName' | ||
|
||
export const googleSheetsRouter = router({ | ||
getAccessToken, | ||
getSpreadsheetName, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
...lder/src/features/blocks/integrations/googleSheets/components/GoogleSpreadsheetPicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { FileIcon } from '@/components/icons' | ||
import { trpc } from '@/lib/trpc' | ||
import { Button, Flex, HStack, IconButton, Text } from '@chakra-ui/react' | ||
import { env } from '@typebot.io/env' | ||
import React, { useEffect, useState } from 'react' | ||
import { GoogleSheetsLogo } from './GoogleSheetsLogo' | ||
import { isDefined } from '@typebot.io/lib' | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
declare const window: any | ||
|
||
type Props = { | ||
spreadsheetId?: string | ||
credentialsId: string | ||
workspaceId: string | ||
onSpreadsheetIdSelect: (spreadsheetId: string) => void | ||
} | ||
|
||
export const GoogleSpreadsheetPicker = ({ | ||
spreadsheetId, | ||
workspaceId, | ||
credentialsId, | ||
onSpreadsheetIdSelect, | ||
}: Props) => { | ||
const [isPickerInitialized, setIsPickerInitialized] = useState(false) | ||
|
||
const { data } = trpc.sheets.getAccessToken.useQuery({ | ||
workspaceId, | ||
credentialsId, | ||
}) | ||
const { data: spreadsheetData, status } = | ||
trpc.sheets.getSpreadsheetName.useQuery( | ||
{ | ||
workspaceId, | ||
credentialsId, | ||
spreadsheetId: spreadsheetId as string, | ||
}, | ||
{ enabled: !!spreadsheetId } | ||
) | ||
|
||
useEffect(() => { | ||
loadScript('gapi', 'https://apis.google.com/js/api.js', () => { | ||
window.gapi.load('picker', () => { | ||
setIsPickerInitialized(true) | ||
}) | ||
}) | ||
}, []) | ||
|
||
const loadScript = ( | ||
id: string, | ||
src: string, | ||
callback: { (): void; (): void; (): void } | ||
) => { | ||
const existingScript = document.getElementById(id) | ||
if (existingScript) { | ||
callback() | ||
return | ||
} | ||
const script = document.createElement('script') | ||
script.type = 'text/javascript' | ||
|
||
script.onload = function () { | ||
callback() | ||
} | ||
|
||
script.src = src | ||
document.head.appendChild(script) | ||
} | ||
|
||
const createPicker = () => { | ||
if (!data) return | ||
if (!isPickerInitialized) throw new Error('Google Picker not inited') | ||
|
||
const picker = new window.google.picker.PickerBuilder() | ||
.addView(window.google.picker.ViewId.SPREADSHEETS) | ||
.setOAuthToken(data.accessToken) | ||
.setDeveloperKey(env.NEXT_PUBLIC_GOOGLE_API_KEY) | ||
.setCallback(pickerCallback) | ||
.build() | ||
|
||
picker.setVisible(true) | ||
} | ||
|
||
const pickerCallback = (data: { action: string; docs: { id: string }[] }) => { | ||
if (data.action !== 'picked') return | ||
const spreadsheetId = data.docs[0]?.id | ||
if (!spreadsheetId) return | ||
onSpreadsheetIdSelect(spreadsheetId) | ||
} | ||
|
||
if (spreadsheetData && spreadsheetData.name !== '') | ||
return ( | ||
<Flex justifyContent="space-between"> | ||
<HStack spacing={2}> | ||
<GoogleSheetsLogo /> | ||
<Text fontWeight="semibold">{spreadsheetData.name}</Text> | ||
</HStack> | ||
<IconButton | ||
size="sm" | ||
icon={<FileIcon />} | ||
onClick={createPicker} | ||
isLoading={!isPickerInitialized} | ||
aria-label={'Pick another spreadsheet'} | ||
/> | ||
</Flex> | ||
) | ||
return ( | ||
<Button | ||
onClick={createPicker} | ||
isLoading={ | ||
!isPickerInitialized || | ||
(isDefined(spreadsheetId) && status === 'loading') | ||
} | ||
> | ||
Pick a spreadsheet | ||
</Button> | ||
) | ||
} |
Oops, something went wrong.
deab1a1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
docs – ./apps/docs
docs-typebot-io.vercel.app
docs-git-main-typebot-io.vercel.app
docs.typebot.io
deab1a1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
viewer-v2 – ./apps/viewer
saquegov.site
shop.mexwa.my
signup.cr8.ai
start.taxt.co
thegymgame.it
theusm.com.br
turkey.cr8.ai
vhpage.cr8.ai
vitamyway.com
webwhats.chat
whatchat.site
www.wiccom.it
acessovip.shop
adsgrow.com.br
am.nigerias.io
an.nigerias.io
app.yvon.earth
ar.nigerias.io
bot.enreso.org
bot.mail2wa.me
bot.rslabs.pro
bot.share5.net
bot.sussmeg.hu
bot.wachap.app
bots.bng.tools
bots.bridge.ai
chad.gocto.com
chat.ftplay.me
chat.hayuri.id
chat.uprize.hu
chatgpt.lam.ee
chicken.cr8.ai
debitozero.com
drayumi.social
gollum.riku.ai
gsbulletin.com
journey.cr8.ai
kopibayane.com
panther.cr7.ai
panther.cr8.ai
pay.sifuim.com
penguin.cr8.ai
petaikorea.com
privetvplus.me
segredomeu.com
semaknilai.com
talk.gocare.io
ticketfute.com
unicorn.cr8.ai
whats-app.chat
survey.collab.day
test.eqfeqfeq.com
titan.jetdigi.com
viewer.typebot.io
welcome.triplo.ai
www.thegymgame.it
zeropendencia.com
1988.bouclidom.com
a.onewebcenter.com
acordorenovado.com
amostra-safe.click
andreimayer.com.br
bebesemcolicas.com
beneficiobr.online
bot.codefree.space
bot.flowmattic.xyz
bot.innovacion.fun
bot.jogodospix.com
bot.jogomilion.com
bot.lucide.contact
bot.mailerflow.com
bot.neferlopez.com
bot.newaiguide.com
bot.photonative.de
bot.rajatanjak.com
bot.samplehunt.com
bot.sinalcerto.com
bot.smoothmenu.com
bot.wphelpchat.com
bots.robomotion.io
brandingmkt.com.br
bt.chatgptlabs.org
cadu.uninta.edu.br
chat-do-cidadao.me
chat.daftarjer.com
chat.foxbot.online
chat.hand-made.one
chat.thausdisc.com
chat.tuanpakya.com
chat.webisharp.com
chatbotforthat.com
cibellyprof.com.br
descobrindotudo.me
dicanatural.online
digitalhelp.com.au
draraquelnutri.com
drcarlosyoshi.site
facilitebid.online
goalsettingbot.com
golpenuncamais.com
deab1a1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
builder-v2 – ./apps/builder
builder-v2-typebot-io.vercel.app
builder-v2-git-main-typebot-io.vercel.app
app.typebot.io
deab1a1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
landing-page-v2 – ./apps/landing-page
landing-page-v2-git-main-typebot-io.vercel.app
landing-page-v2-typebot-io.vercel.app
www.typebot.io
get-typebot.com
home.typebot.io
www.get-typebot.com