diff --git a/apps/builder/.env.local.example b/apps/builder/.env.local.example index c35465d98..0bf9c9803 100644 --- a/apps/builder/.env.local.example +++ b/apps/builder/.env.local.example @@ -1,5 +1,3 @@ -NEXT_PUBLIC_AUTH_MOCKING=disabled - DATABASE_URL=postgresql://postgres:@localhost:5432/typebot ENCRYPTION_SECRET=q3t6v9y$B&E)H@McQfTjWnZr4u7x!z%C #256-bits secret (can be generated here: https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) diff --git a/apps/builder/components/dashboard/FolderContent.tsx b/apps/builder/components/dashboard/FolderContent.tsx index 44882fefc..fd48bcac7 100644 --- a/apps/builder/components/dashboard/FolderContent.tsx +++ b/apps/builder/components/dashboard/FolderContent.tsx @@ -12,10 +12,13 @@ import { Wrap, } from '@chakra-ui/react' import { useTypebotDnd } from 'contexts/TypebotDndContext' -import { Typebot } from 'models' import React, { useEffect, useState } from 'react' import { createFolder, useFolders } from 'services/folders' -import { patchTypebot, useTypebots } from 'services/typebots' +import { + patchTypebot, + TypebotInDashboard, + useTypebots, +} from 'services/typebots' import { AnnoucementModal } from './annoucements/AnnoucementModal' import { BackButton } from './FolderContent/BackButton' import { CreateBotButton } from './FolderContent/CreateBotButton' @@ -42,7 +45,8 @@ export const FolderContent = ({ folder }: Props) => { x: 0, y: 0, }) - const [typebotDragCandidate, setTypebotDragCandidate] = useState() + const [typebotDragCandidate, setTypebotDragCandidate] = + useState() const { isOpen, onOpen, onClose } = useDisclosure() const toast = useToast({ @@ -130,16 +134,17 @@ export const FolderContent = ({ folder }: Props) => { } useEventListener('mouseup', handleMouseUp) - const handleMouseDown = (typebot: Typebot) => (e: React.MouseEvent) => { - const element = e.currentTarget as HTMLDivElement - const rect = element.getBoundingClientRect() - setDraggablePosition({ x: rect.left, y: rect.top }) - const x = e.clientX - rect.left - const y = e.clientY - rect.top - setRelativeDraggablePosition({ x, y }) - setMouseDownPosition({ x: e.screenX, y: e.screenY }) - setTypebotDragCandidate(typebot) - } + const handleMouseDown = + (typebot: TypebotInDashboard) => (e: React.MouseEvent) => { + const element = e.currentTarget as HTMLDivElement + const rect = element.getBoundingClientRect() + setDraggablePosition({ x: rect.left, y: rect.top }) + const x = e.clientX - rect.left + const y = e.clientY - rect.top + setRelativeDraggablePosition({ x, y }) + setMouseDownPosition({ x: e.screenX, y: e.screenY }) + setTypebotDragCandidate(typebot) + } const handleMouseMove = (e: MouseEvent) => { if (!typebotDragCandidate) return diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx index e5ef4c02d..2bbfb1713 100644 --- a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx +++ b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx @@ -21,7 +21,7 @@ import { useTypebotDnd } from 'contexts/TypebotDndContext' import { useDebounce } from 'use-debounce' type ChatbotCardProps = { - typebot: Typebot + typebot: Pick onTypebotDeleted: () => void onMouseDown: (e: React.MouseEvent) => void } @@ -66,7 +66,7 @@ export const TypebotButton = ({ const handleDuplicateClick = async (e: React.MouseEvent) => { e.stopPropagation() - const { data: createdTypebot, error } = await duplicateTypebot(typebot) + const { data: createdTypebot, error } = await duplicateTypebot(typebot.id) if (error) return toast({ title: "Couldn't duplicate typebot", diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx index 18aaec10c..78dcf6095 100644 --- a/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx +++ b/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx @@ -1,9 +1,9 @@ import { Box, BoxProps, Flex, Text, VStack } from '@chakra-ui/react' import { GlobeIcon, ToolIcon } from 'assets/icons' -import { Typebot } from 'models' +import { TypebotInDashboard } from 'services/typebots' type Props = { - typebot: Typebot + typebot: TypebotInDashboard } & BoxProps export const TypebotCardOverlay = ({ typebot, ...props }: Props) => { diff --git a/apps/builder/contexts/TypebotDndContext.tsx b/apps/builder/contexts/TypebotDndContext.tsx index 0eb42daa8..dbb6ae81e 100644 --- a/apps/builder/contexts/TypebotDndContext.tsx +++ b/apps/builder/contexts/TypebotDndContext.tsx @@ -1,4 +1,3 @@ -import { Typebot } from 'models' import { createContext, Dispatch, @@ -8,10 +7,11 @@ import { useEffect, useState, } from 'react' +import { TypebotInDashboard } from 'services/typebots' const typebotDndContext = createContext<{ - draggedTypebot?: Typebot - setDraggedTypebot: Dispatch> + draggedTypebot?: TypebotInDashboard + setDraggedTypebot: Dispatch> mouseOverFolderId?: string | null setMouseOverFolderId: Dispatch> // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -19,7 +19,7 @@ const typebotDndContext = createContext<{ }>({}) export const TypebotDndContext = ({ children }: { children: ReactNode }) => { - const [draggedTypebot, setDraggedTypebot] = useState() + const [draggedTypebot, setDraggedTypebot] = useState() const [mouseOverFolderId, setMouseOverFolderId] = useState() useEffect(() => { diff --git a/apps/builder/pages/api/auth/[...nextauth].ts b/apps/builder/pages/api/auth/[...nextauth].ts index d4f112186..bf492a270 100644 --- a/apps/builder/pages/api/auth/[...nextauth].ts +++ b/apps/builder/pages/api/auth/[...nextauth].ts @@ -10,6 +10,7 @@ import { NextApiRequest, NextApiResponse } from 'next' import { isNotDefined } from 'utils' import { User } from 'db' import { randomUUID } from 'crypto' +import { withSentry } from '@sentry/nextjs' const providers: Provider[] = [ EmailProvider({ @@ -77,4 +78,4 @@ const generateApiToken = async (userId: string) => { return apiToken } -export default handler +export default withSentry(handler) diff --git a/apps/builder/pages/api/integrations/email/test-config.ts b/apps/builder/pages/api/integrations/email/test-config.ts index d00f1643a..7e17003e6 100644 --- a/apps/builder/pages/api/integrations/email/test-config.ts +++ b/apps/builder/pages/api/integrations/email/test-config.ts @@ -1,3 +1,4 @@ +import { withSentry } from '@sentry/nextjs' import { SmtpCredentialsData } from 'models' import { NextApiRequest, NextApiResponse } from 'next' import { createTransport } from 'nodemailer' @@ -25,4 +26,4 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } -export default handler +export default withSentry(handler) diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts index 071ba0225..9fff12a1d 100644 --- a/apps/builder/pages/api/typebots.ts +++ b/apps/builder/pages/api/typebots.ts @@ -21,6 +21,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { ownerId: user.id, folderId, }, + select: { name: true, publishedTypebotId: true, id: true }, }) return res.send({ typebots }) } diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts index 562af9ed8..565dd1fa5 100644 --- a/apps/builder/services/typebots.ts +++ b/apps/builder/services/typebots.ts @@ -51,6 +51,10 @@ import { stringify } from 'qs' import { isChoiceInput, isConditionStep, sendRequest } from 'utils' import { parseBlocksToPublicBlocks } from './publicTypebot' +export type TypebotInDashboard = Pick< + Typebot, + 'id' | 'name' | 'publishedTypebotId' +> export const useTypebots = ({ folderId, onError, @@ -59,11 +63,10 @@ export const useTypebots = ({ onError: (error: Error) => void }) => { const params = stringify({ folderId }) - const { data, error, mutate } = useSWR<{ typebots: Typebot[] }, Error>( - `/api/typebots?${params}`, - fetcher, - { dedupingInterval: 0 } - ) + const { data, error, mutate } = useSWR< + { typebots: TypebotInDashboard[] }, + Error + >(`/api/typebots?${params}`, fetcher, { dedupingInterval: 0 }) if (error) onError(error) return { typebots: data?.typebots, @@ -93,11 +96,13 @@ export const importTypebot = async (typebot: Typebot) => body: typebot, }) -export const duplicateTypebot = async (typebot: Typebot) => { +export const duplicateTypebot = async (typebotId: string) => { + const { data: typebotToDuplicate } = await getTypebot(typebotId) + if (!typebotToDuplicate) return { error: new Error('Typebot not found') } const duplicatedTypebot: Omit = omit( { - ...typebot, - name: `${typebot.name} copy`, + ...typebotToDuplicate, + name: `${typebotToDuplicate.name} copy`, publishedTypebotId: null, publicId: null, }, @@ -110,6 +115,12 @@ export const duplicateTypebot = async (typebot: Typebot) => { }) } +const getTypebot = (typebotId: string) => + sendRequest({ + url: `/api/typebots/${typebotId}`, + method: 'GET', + }) + export const deleteTypebot = async (id: string) => sendRequest({ url: `/api/typebots/${id}`, diff --git a/apps/viewer/pages/api/typebots.ts b/apps/viewer/pages/api/typebots.ts new file mode 100644 index 000000000..d7b928aea --- /dev/null +++ b/apps/viewer/pages/api/typebots.ts @@ -0,0 +1,20 @@ +import { withSentry } from '@sentry/nextjs' +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { authenticateUser } from 'services/api/utils' +import { methodNotAllowed } from 'utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'GET') { + const user = await authenticateUser(req) + if (!user) return res.status(401).json({ message: 'Not authenticated' }) + const typebots = await prisma.typebot.findMany({ + where: { ownerId: user.id }, + select: { name: true, publishedTypebotId: true, id: true }, + }) + return res.send({ typebots }) + } + return methodNotAllowed(res) +} + +export default withSentry(handler) diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts new file mode 100644 index 000000000..db44e838f --- /dev/null +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts @@ -0,0 +1,57 @@ +import { withSentry } from '@sentry/nextjs' +import { Prisma } from 'db' +import prisma from 'libs/prisma' +import { IntegrationStepType, Typebot } from 'models' +import { NextApiRequest, NextApiResponse } from 'next' +import { authenticateUser } from 'services/api/utils' +import { methodNotAllowed } from 'utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'PATCH') { + const user = await authenticateUser(req) + if (!user) return res.status(401).json({ message: 'Not authenticated' }) + const body = req.body as Record + if (!('url' in body)) + return res.status(403).send({ message: 'url is missing in body' }) + const { url } = body + const typebotId = req.query.typebotId.toString() + const stepId = req.query.stepId.toString() + const typebot = (await prisma.typebot.findUnique({ + where: { id_ownerId: { id: typebotId, ownerId: user.id } }, + })) as Typebot | undefined + if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) + try { + const updatedTypebot = addUrlToWebhookStep(url, typebot, stepId) + await prisma.typebot.update({ + where: { id_ownerId: { id: typebotId, ownerId: user.id } }, + data: { blocks: updatedTypebot.blocks as Prisma.JsonArray }, + }) + return res.send({ message: 'success' }) + } catch (err) { + return res + .status(400) + .send({ message: "stepId doesn't point to a Webhook step" }) + } + } + return methodNotAllowed(res) +} + +const addUrlToWebhookStep = ( + url: string, + typebot: Typebot, + stepId: string +): Typebot => ({ + ...typebot, + blocks: typebot.blocks.map((b) => ({ + ...b, + steps: b.steps.map((s) => { + if (s.id === stepId) { + if (s.type !== IntegrationStepType.WEBHOOK) throw new Error() + return { ...s, webhook: { ...s.webhook, url } } + } + return s + }), + })), +}) + +export default withSentry(handler) diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts new file mode 100644 index 000000000..a1265a02e --- /dev/null +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts @@ -0,0 +1,55 @@ +import { withSentry } from '@sentry/nextjs' +import { Prisma } from 'db' +import prisma from 'libs/prisma' +import { IntegrationStepType, Typebot } from 'models' +import { NextApiRequest, NextApiResponse } from 'next' +import { authenticateUser } from 'services/api/utils' +import { omit } from 'services/utils' +import { methodNotAllowed } from 'utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'DELETE') { + const user = await authenticateUser(req) + if (!user) return res.status(401).json({ message: 'Not authenticated' }) + const typebotId = req.query.typebotId.toString() + const stepId = req.query.stepId.toString() + const typebot = (await prisma.typebot.findUnique({ + where: { id_ownerId: { id: typebotId, ownerId: user.id } }, + })) as Typebot | undefined + if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) + try { + const updatedTypebot = removeUrlFromWebhookStep(typebot, stepId) + await prisma.typebot.update({ + where: { id_ownerId: { id: typebotId, ownerId: user.id } }, + data: { + blocks: updatedTypebot.blocks as Prisma.JsonArray, + }, + }) + return res.send({ message: 'success' }) + } catch (err) { + return res + .status(400) + .send({ message: "stepId doesn't point to a Webhook step" }) + } + } + return methodNotAllowed(res) +} + +const removeUrlFromWebhookStep = ( + typebot: Typebot, + stepId: string +): Typebot => ({ + ...typebot, + blocks: typebot.blocks.map((b) => ({ + ...b, + steps: b.steps.map((s) => { + if (s.id === stepId) { + if (s.type !== IntegrationStepType.WEBHOOK) throw new Error() + return { ...s, webhook: omit(s.webhook, 'url') } + } + return s + }), + })), +}) + +export default withSentry(handler) diff --git a/apps/viewer/pages/api/typebots/[typebotId]/webhookSteps.ts b/apps/viewer/pages/api/typebots/[typebotId]/webhookSteps.ts new file mode 100644 index 000000000..d740a091e --- /dev/null +++ b/apps/viewer/pages/api/typebots/[typebotId]/webhookSteps.ts @@ -0,0 +1,37 @@ +import { withSentry } from '@sentry/nextjs' +import prisma from 'libs/prisma' +import { Block, IntegrationStepType } from 'models' +import { NextApiRequest, NextApiResponse } from 'next' +import { authenticateUser } from 'services/api/utils' +import { methodNotAllowed } from 'utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'GET') { + const user = await authenticateUser(req) + if (!user) return res.status(401).json({ message: 'Not authenticated' }) + const typebotId = req.query.typebotId.toString() + const typebot = await prisma.typebot.findUnique({ + where: { id_ownerId: { id: typebotId, ownerId: user.id } }, + select: { blocks: true }, + }) + const emptyWebhookSteps = (typebot?.blocks as Block[]).reduce< + { blockId: string; stepId: string; name: string }[] + >((emptyWebhookSteps, block) => { + const steps = block.steps.filter( + (step) => step.type === IntegrationStepType.WEBHOOK && !step.webhook.url + ) + return [ + ...emptyWebhookSteps, + ...steps.map((s) => ({ + blockId: s.blockId, + stepId: s.id, + name: `${block.title} > ${s.id}`, + })), + ] + }, []) + return res.send({ steps: emptyWebhookSteps }) + } + return methodNotAllowed(res) +} + +export default withSentry(handler) diff --git a/apps/viewer/pages/api/users/me.ts b/apps/viewer/pages/api/users/me.ts new file mode 100644 index 000000000..b1390b8ff --- /dev/null +++ b/apps/viewer/pages/api/users/me.ts @@ -0,0 +1,16 @@ +import { withSentry } from '@sentry/nextjs' +import { NextApiRequest, NextApiResponse } from 'next' +import { authenticateUser } from 'services/api/utils' +import { isNotDefined, methodNotAllowed } from 'utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'GET') { + const user = await authenticateUser(req) + if (isNotDefined(user)) + return res.status(404).send({ message: 'User not found' }) + return res.send({ id: user.id }) + } + return methodNotAllowed(res) +} + +export default withSentry(handler) diff --git a/apps/viewer/playwright/services/database.ts b/apps/viewer/playwright/services/database.ts index 3410b0f78..dc52eefa8 100644 --- a/apps/viewer/playwright/services/database.ts +++ b/apps/viewer/playwright/services/database.ts @@ -1,6 +1,5 @@ import { Block, - CredentialsType, defaultSettings, defaultTheme, PublicBlock, @@ -8,39 +7,31 @@ import { Step, Typebot, } from 'models' -import { PrismaClient, User } from 'db' +import { PrismaClient } from 'db' import { readFileSync } from 'fs' -import { encrypt } from 'utils' const prisma = new PrismaClient() -export const teardownDatabase = async () => { - const ownerFilter = { - where: { ownerId: { in: ['freeUser', 'proUser'] } }, - } - await prisma.user.deleteMany({ - where: { id: { in: ['freeUser', 'proUser'] } }, - }) - await prisma.credentials.deleteMany(ownerFilter) - return prisma.typebot.deleteMany(ownerFilter) +export const teardownDatabase = () => { + try { + return prisma.user.delete({ + where: { id: 'user' }, + }) + } catch {} } -export const setupDatabase = async () => { - await createUsers() - return createCredentials() -} +export const setupDatabase = () => createUser() -export const createUsers = () => - prisma.user.createMany({ - data: [ - { id: 'freeUser', email: 'free-user@email.com', name: 'Free user' }, - { id: 'proUser', email: 'pro-user@email.com', name: 'Pro user' }, - ], +export const createUser = () => + prisma.user.create({ + data: { + id: 'user', + email: 'user@email.com', + name: 'User', + apiToken: 'userToken', + }, }) -export const getSignedInUser = (email: string) => - prisma.user.findFirst({ where: { email } }) - export const createTypebots = async (partialTypebots: Partial[]) => { await prisma.typebot.createMany({ data: partialTypebots.map(parseTestTypebot) as any[], @@ -52,36 +43,6 @@ export const createTypebots = async (partialTypebots: Partial[]) => { }) } -const createCredentials = () => { - const { encryptedData, iv } = encrypt({ - expiry_date: 1642441058842, - access_token: - 'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod', - // This token is linked to a test Google account (typebot.test.user@gmail.com) - refresh_token: - '1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29G69iEhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek', - }) - return prisma.credentials.createMany({ - data: [ - { - name: 'test2@gmail.com', - ownerId: 'proUser', - type: CredentialsType.GOOGLE_SHEETS, - data: encryptedData, - iv, - }, - ], - }) -} - -export const updateUser = (data: Partial) => - prisma.user.update({ - data, - where: { - id: 'proUser', - }, - }) - const parseTypebotToPublicTypebot = ( id: string, typebot: Typebot @@ -110,7 +71,7 @@ const parseTestTypebot = (partialTypebot: Partial): Typebot => ({ id: partialTypebot.id ?? 'typebot', folderId: null, name: 'My typebot', - ownerId: 'proUser', + ownerId: 'user', theme: defaultTheme, settings: defaultSettings, createdAt: new Date(), @@ -172,7 +133,7 @@ export const importTypebotInDatabase = async ( const typebot: any = { ...JSON.parse(readFileSync(path).toString()), ...updates, - ownerId: 'proUser', + ownerId: 'user', } await prisma.typebot.create({ data: typebot, diff --git a/apps/viewer/playwright/tests/api.spec.ts b/apps/viewer/playwright/tests/api.spec.ts new file mode 100644 index 000000000..ef3fb9556 --- /dev/null +++ b/apps/viewer/playwright/tests/api.spec.ts @@ -0,0 +1,100 @@ +import test, { expect } from '@playwright/test' +import { createTypebots, parseDefaultBlockWithStep } from '../services/database' +import { + IntegrationStepType, + defaultWebhookOptions, + defaultWebhookAttributes, +} from 'models' + +const typebotId = 'webhook-flow' +test.beforeAll(async () => { + try { + await createTypebots([ + { + id: typebotId, + ...parseDefaultBlockWithStep({ + type: IntegrationStepType.WEBHOOK, + options: defaultWebhookOptions, + webhook: { id: 'webhookId', ...defaultWebhookAttributes }, + }), + }, + ]) + } catch (err) {} +}) + +test('can list typebots', async ({ request }) => { + expect((await request.get(`/api/typebots`)).status()).toBe(401) + const response = await request.get(`/api/typebots`, { + headers: { Authorization: 'Bearer userToken' }, + }) + const { typebots } = await response.json() + expect(typebots).toHaveLength(1) + expect(typebots[0]).toMatchObject({ + id: typebotId, + publishedTypebotId: null, + name: 'My typebot', + }) +}) + +test('can get webhook steps', async ({ request }) => { + expect( + (await request.get(`/api/typebots/${typebotId}/webhookSteps`)).status() + ).toBe(401) + const response = await request.get( + `/api/typebots/${typebotId}/webhookSteps`, + { + headers: { Authorization: 'Bearer userToken' }, + } + ) + const { steps } = await response.json() + expect(steps).toHaveLength(1) + expect(steps[0]).toEqual({ + stepId: 'step1', + blockId: 'block1', + name: 'Block #1 > step1', + }) +}) + +test('can subscribe webhook', async ({ request }) => { + expect( + ( + await request.patch( + `/api/typebots/${typebotId}/blocks/block1/steps/step1/subscribeWebhook`, + { data: { url: 'https://test.com' } } + ) + ).status() + ).toBe(401) + const response = await request.patch( + `/api/typebots/${typebotId}/blocks/block1/steps/step1/subscribeWebhook`, + { + headers: { + Authorization: 'Bearer userToken', + }, + data: { url: 'https://test.com' }, + } + ) + const body = await response.json() + expect(body).toEqual({ + message: 'success', + }) +}) + +test('can unsubscribe webhook', async ({ request }) => { + expect( + ( + await request.delete( + `/api/typebots/${typebotId}/blocks/block1/steps/step1/unsubscribeWebhook` + ) + ).status() + ).toBe(401) + const response = await request.delete( + `/api/typebots/${typebotId}/blocks/block1/steps/step1/unsubscribeWebhook`, + { + headers: { Authorization: 'Bearer userToken' }, + } + ) + const body = await response.json() + expect(body).toEqual({ + message: 'success', + }) +}) diff --git a/apps/viewer/services/api/utils.ts b/apps/viewer/services/api/utils.ts new file mode 100644 index 000000000..86502ab0d --- /dev/null +++ b/apps/viewer/services/api/utils.ts @@ -0,0 +1,17 @@ +import { User } from 'db' +import prisma from 'libs/prisma' +import { NextApiRequest } from 'next' + +export const authenticateUser = async ( + req: NextApiRequest +): Promise => authenticateByToken(extractBearerToken(req)) + +const authenticateByToken = async ( + apiToken?: string +): Promise => { + if (!apiToken) return + return (await prisma.user.findFirst({ where: { apiToken } })) as User +} + +const extractBearerToken = (req: NextApiRequest) => + req.headers['authorization']?.slice(7) diff --git a/apps/viewer/services/utils.ts b/apps/viewer/services/utils.ts new file mode 100644 index 000000000..5ff354bf4 --- /dev/null +++ b/apps/viewer/services/utils.ts @@ -0,0 +1,19 @@ +interface Omit { + // eslint-disable-next-line @typescript-eslint/ban-types + (obj: T, ...keys: K): { + [K2 in Exclude]: T[K2] + } +} + +export const omit: Omit = (obj, ...keys) => { + const ret = {} as { + [K in keyof typeof obj]: typeof obj[K] + } + let key: keyof typeof obj + for (key in obj) { + if (!keys.includes(key)) { + ret[key] = obj[key] + } + } + return ret +} diff --git a/package.json b/package.json index c381b3621..3123298cd 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,13 @@ "docker:up": "docker-compose up -d", "db:nuke": "docker-compose down --volumes --remove-orphans", "dev": "yarn docker:up && turbo run dev --parallel", + "dev:mocking": "yarn docker:up && NEXT_PUBLIC_AUTH_MOCKING=enabled turbo run dev --parallel", "build": "yarn docker:up && turbo run build", "test:builder": "cd apps/builder && yarn test", "lint": "turbo run lint" }, "devDependencies": { - "dotenv-cli": "^4.1.1", - "turbo": "^1.1.2" + "turbo": "^1.1.3" }, "packageManager": "yarn@1.22.17" } diff --git a/packages/db/package.json b/packages/db/package.json index 583a6cba2..f34343459 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -7,7 +7,8 @@ "devDependencies": { "prisma": "^3.9.2", "ts-node": "^10.5.0", - "typescript": "^4.5.5" + "typescript": "^4.5.5", + "dotenv-cli": "5.0.0" }, "dependencies": { "@prisma/client": "^3.9.2" diff --git a/yarn.lock b/yarn.lock index f25e17673..c6014e3f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6809,31 +6809,26 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" -dotenv-cli@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-4.1.1.tgz#26a59fbb25876008985a15fa366b416607e8372c" - integrity sha512-XvKv1pa+UBrsr3CtLGBsR6NdsoS7znqaHUf4Knj0eZO+gOI/hjj9KgWDP+KjpfEbj6wAba1UpbhaP9VezNkWhg== +dotenv-cli@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-5.0.0.tgz#13ff77794096b00968d96187e4c8674e045461f3" + integrity sha512-0Cb2WMDJ805hTD7m43gXXFLraoE5KwrKmGW2dAzYvSEB96tlKI2hmcJ/9In4s2FfvkAFk3SjNQcLeKLoRSXhKA== dependencies: - cross-spawn "^7.0.1" - dotenv "^8.1.0" - dotenv-expand "^5.1.0" - minimist "^1.1.3" + cross-spawn "^7.0.3" + dotenv "^16.0.0" + dotenv-expand "^8.0.1" + minimist "^1.2.5" -dotenv-expand@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" - integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== +dotenv-expand@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.1.tgz#332aa17c14b12e28e2e230f8d183eecc1c014fdc" + integrity sha512-j/Ih7bIERDR5PzI89Zu8ayd3tXZ6E3dbY0ljQ9Db0K87qBO8zdLsi2dIvDHMWtjC3Yxb8XixOTHAtia0fDHRpg== dotenv@^16.0.0: version "16.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== -dotenv@^8.1.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -10202,7 +10197,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -13787,83 +13782,83 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -turbo-darwin-64@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.1.2.tgz#b129aaf538821de78d5e2129495c553627174650" - integrity sha512-rua17HnVvAqAU54gVfiQoH7cfopOqANv+yI6NtxLMD8aFfX2cJ9m8SSvH2v2vCaToNDW6OnTkdqDKQpqIHzbCw== +turbo-darwin-64@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.1.3.tgz#f94feceba0c15966a701193a587bd6b63efbf538" + integrity sha512-dIJE19hY6FmCoIvXWa6RO85xLWTw0tsDZlOdUAE+EK5YM54G8yIsfZqeVZdn5Iy28oMOvpBoF1TL6936d3zaXQ== -turbo-darwin-arm64@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.1.2.tgz#07a783ad2e3e8af600ae7406cc4062ff56ac0351" - integrity sha512-otqSQNYDyKg0KqB3NM0BI4oiRPKdJkUE/XBn8dcUS+zeRLrL00XtaM0eSwynZs1tb6zU/Y+SPMSBRygD1TCOnw== +turbo-darwin-arm64@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.1.3.tgz#8d83a8990abbde5607085047211bc3b40e1d8bb2" + integrity sha512-6AtJD0TxtxSiWlgPZbr3OltNbdhjq3Tuowi2sgVdYXB1dEYoHn4l5Fa7Nv5lr72X1OvItmmEcqSLCqi+uBf1PQ== -turbo-freebsd-64@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.1.2.tgz#9e22abf04ec2298f205a57b5c9ce14e22844baf3" - integrity sha512-2nxwVDTAM0DtIQ2i3UOfEsQLF7vp+XZ/b9SKtiHxz710fXvdyuGivYI25axDdcBn8kQ45rnbUnarF1aW8CMGgg== +turbo-freebsd-64@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.1.3.tgz#6b37aa798201ba2a5b41c4c890d38e3b5f3c8a2f" + integrity sha512-dzYlYWK/5nwZaABRNwYg9sSNvC2QHcEU3WZdsZwviRsyAG1O4bxStnhR22BAzJs+jxRUrG3W7j1pUY1rqdJ62Q== -turbo-freebsd-arm64@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.1.2.tgz#6095c9012881225a5fdfb55362defa12f24b1f8e" - integrity sha512-ro1Ah96yzgzyT0BJe1mceAqxPxi0pUwzAvN3IKVpMqi4hYkT3aRbzDCaSxzyC6let2Al/NUsgHnbAv38OF2Xkw== +turbo-freebsd-arm64@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.1.3.tgz#34c07c7c8200a17e9c931b02cac14947cd8d6533" + integrity sha512-YZ/bBy59/16hEb06G73m5IcuMRCFRZnmEXMMlmfY2B+AR5d103TdenZQ0sxWWRVVQu7FfhT3QsbJ00GcxtrO8Q== -turbo-linux-32@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.1.2.tgz#4726e533d6966172b6bc4a960524ec2eb61adaab" - integrity sha512-HKBsETxQMVaf/DJwMg7pypPbGA6KEu0gEf9C8o2aPJvwMPBYgNsNaU08Xizuh5xzEQTzpbIWfQyqdNgMV4RG3Q== +turbo-linux-32@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.1.3.tgz#ab2b6d163f781f24d4b9c283a195fe1e3ab47c53" + integrity sha512-BwP8oL9NlhlIFgEqBfsj7ASx5Adzaf1L7Xn9GGhv2v2uD1N8mgAuPyy/X3VUYDdMi6JeHc5gxRKnawJI7uTr9g== -turbo-linux-64@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.1.2.tgz#dfe7f3a4c91acecdb84ecab330acee06857e568e" - integrity sha512-IklKsOklcRHIWkTzKg95BQ6jgJ53kLvRMrp8yqzlvZprkWdiyhAgUxrUTTHOOTce2XA3+jdN2+MwixG44uY2vg== +turbo-linux-64@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.1.3.tgz#48f2bf622d1d7a450a865598c800b4a7e340977e" + integrity sha512-ZdgaJO45ZHxJStDU6uSSgw1baCB1OatF9YOgx6XByOIeV0etdETFvhTPhSZ/mD9Xsk5QP1SoCjrvTbhk6/FZsg== -turbo-linux-arm64@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.1.2.tgz#c39b6c50657fa0e82627407c86a5c43f19598e2b" - integrity sha512-3kS6sk2lOtuBBqkcL+yeGqD1yew4UZ1o7XUcbDD8UPwhF2kAfK7Qs0vTJw4lnO1scjhihkoTrmXM7yozvjf4/w== +turbo-linux-arm64@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.1.3.tgz#0938893ddae8a450ce364cf0640a9f8c27aecac0" + integrity sha512-gM9638BGZ2An94cd8w/Q35xCUmo/ZACcQJnmR82pyRUIalk3tmGnKupjn1IFK8yCPdMJ4+zs5SvrLGn/C+ldtQ== -turbo-linux-arm@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.1.2.tgz#2f51f93a3aa144b8ba25d7b0e3c53ea186a0e9dd" - integrity sha512-CNbaTvRozq7H/5jpy9OZlzJ6BkeEXF+nF2n9dHiUrbAXd3nq84Qt9odcQJmGnexP19YS9w6l3tIHncX4BgwtqA== +turbo-linux-arm@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.1.3.tgz#369e758ef715f5560efd0ecf419f8d168f337a39" + integrity sha512-ZBce2TXUgt8IPyXcNMlR07fvRLoIh6VhDR+7zbL1tSAtWkOTGQHS0h80B7bg6pQ4IftQqluGc8NkMaNCUmDgaA== -turbo-linux-mips64le@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.1.2.tgz#f52b7f410ac289d4e539f108679d2324aa5e271e" - integrity sha512-CDoXVIlW43C6KLgYxe13KkG8h6DswXHxbTVHiZdOwRQ56j46lU+JOVpLoh6wpQGcHvj58VEiypZBRTGVFMeogw== +turbo-linux-mips64le@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.1.3.tgz#517696de0ba5a3861e055d84b75bde09f4539e71" + integrity sha512-9QmdAq+Yl4iGvUJVbcQ4hMH4BPeXqBtkBAOEiPunbeYtRH4xzQh+bSCYdrH7FfujND82nCKVjXMuGSJO6IyY6g== -turbo-linux-ppc64le@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.1.2.tgz#18d08d3414075d0dcb4be83ca837dda508313996" - integrity sha512-xPVMHoiOJE/qI63jSOXwYIUFQXLdstxDV6fLnRxvq0QnJNxgTKq+mLUeE8M4LDVh1bdqHLcfk/HmyQ6+X1XVkQ== +turbo-linux-ppc64le@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.1.3.tgz#2014f7e98752e04d568cfa9cf04038e2d185154d" + integrity sha512-CwWh9V5Cqh4TR8E7mAR2+WLlsyJcLkZzZ6GQdS8rovVveKPII2VpTihMXkGyvwCxO/pQmOHwCaVUzgxIGtQrDQ== -turbo-windows-32@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.1.2.tgz#96033019094bcb091647d6063c3c9b8e83d0acbe" - integrity sha512-Gj1yvPE0aMDSOxGVSBaecLnwsVDT1xX8U0dtLrg52TYY2jlaci0atjHKr9nTFuX7z8uwAf6PopwdriGoCeT3ng== +turbo-windows-32@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.1.3.tgz#7f75c6611a0a1d9d1b15129a452ebfffd77fbcf3" + integrity sha512-/qJoU6P8pBZrTdxiV4yUxyc1yVBJdsZunP9vYuF7tz/3DCaJujnZhmYOn+vcA2fUe9wwk/s7e2eg7LG5qsREFA== -turbo-windows-64@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.1.2.tgz#8eb3f77ab7e04b077752ae2204114c82e5c74697" - integrity sha512-0Ncx/iKhnKrdAU8hJ+8NUcF9jtFr8KoW5mMWfiFzy+mgUbVKbpzWT2eoGR6zJExedQsRvYOejbEX5iihbnj5bA== +turbo-windows-64@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.1.3.tgz#880ed73452e5f405e300c6432aba3dbf68098836" + integrity sha512-tWXUrKUL1Bdx5EekMa0IjonZPjMe1GTHz3/CK3rSI85hkJ/Up52SgcQgkhIs42lMPQiKbrbgISYpVH5c/aLxRw== -turbo@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.1.2.tgz#751b9651dc3ebe469898db76afab6405666ad0ff" - integrity sha512-3ViHKyAkaBKNKwHASTa1zkVT3tVVhQNLrpxBS7LoN+794ouQUYmy6lf0rTqzG3iTZHtIDwC+piZSdTl4XjEVMg== +turbo@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.1.3.tgz#57f5c2639c5780474958daa1432f232c320f5ad0" + integrity sha512-Za/hHiCxGsC3n4yROy5hHLJkJ5tkq2po5V8L+WBURw7RjAt1C7EP5PMIsLQpXV69Icts9NEZnniVRXEkKGuKfQ== optionalDependencies: - turbo-darwin-64 "1.1.2" - turbo-darwin-arm64 "1.1.2" - turbo-freebsd-64 "1.1.2" - turbo-freebsd-arm64 "1.1.2" - turbo-linux-32 "1.1.2" - turbo-linux-64 "1.1.2" - turbo-linux-arm "1.1.2" - turbo-linux-arm64 "1.1.2" - turbo-linux-mips64le "1.1.2" - turbo-linux-ppc64le "1.1.2" - turbo-windows-32 "1.1.2" - turbo-windows-64 "1.1.2" + turbo-darwin-64 "1.1.3" + turbo-darwin-arm64 "1.1.3" + turbo-freebsd-64 "1.1.3" + turbo-freebsd-arm64 "1.1.3" + turbo-linux-32 "1.1.3" + turbo-linux-64 "1.1.3" + turbo-linux-arm "1.1.3" + turbo-linux-arm64 "1.1.3" + turbo-linux-mips64le "1.1.3" + turbo-linux-ppc64le "1.1.3" + turbo-windows-32 "1.1.3" + turbo-windows-64 "1.1.3" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5"