♻️ (viewer) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
643571fe7d
commit
a9d04798bc
120
apps/viewer/src/pages/[[...publicId]].tsx
Normal file
120
apps/viewer/src/pages/[[...publicId]].tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { IncomingMessage } from 'http'
|
||||
import { ErrorPage } from '@/components/ErrorPage'
|
||||
import { NotFoundPage } from '@/components/NotFoundPage'
|
||||
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import { env, getViewerUrl, isDefined, isNotDefined, omit } from 'utils'
|
||||
import { TypebotPage, TypebotPageProps } from '../components/TypebotPage'
|
||||
import prisma from '../lib/prisma'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (
|
||||
context: GetServerSidePropsContext
|
||||
) => {
|
||||
const isIE = /MSIE|Trident/.test(context.req.headers['user-agent'] ?? '')
|
||||
const pathname = context.resolvedUrl.split('?')[0]
|
||||
const { host, forwardedHost } = getHost(context.req)
|
||||
try {
|
||||
if (!host) return { props: {} }
|
||||
const viewerUrls = (getViewerUrl({ returnAll: true }) ?? '').split(',')
|
||||
const isMatchingViewerUrl =
|
||||
env('E2E_TEST') === 'true'
|
||||
? true
|
||||
: viewerUrls.some(
|
||||
(url) =>
|
||||
host.split(':')[0].includes(url.split('//')[1].split(':')[0]) ||
|
||||
(forwardedHost &&
|
||||
forwardedHost
|
||||
.split(':')[0]
|
||||
.includes(url.split('//')[1].split(':')[0]))
|
||||
)
|
||||
const customDomain = `${forwardedHost ?? host}${
|
||||
pathname === '/' ? '' : pathname
|
||||
}`
|
||||
const publishedTypebot = isMatchingViewerUrl
|
||||
? await getTypebotFromPublicId(context.query.publicId?.toString())
|
||||
: await getTypebotFromCustomDomain(customDomain)
|
||||
if (!publishedTypebot)
|
||||
console.log(
|
||||
isMatchingViewerUrl
|
||||
? `Couldn't find publicId: ${context.query.publicId?.toString()}`
|
||||
: `Couldn't find customDomain: ${customDomain}`
|
||||
)
|
||||
const headCode = publishedTypebot?.settings.metadata.customHeadCode
|
||||
return {
|
||||
props: {
|
||||
publishedTypebot,
|
||||
isIE,
|
||||
url: `https://${forwardedHost ?? host}${pathname}`,
|
||||
customHeadCode:
|
||||
isDefined(headCode) && headCode !== ''
|
||||
? sanitizeHtml(headCode, {
|
||||
allowedTags: ['script', 'meta'],
|
||||
allowedAttributes: {
|
||||
meta: ['name', 'content'],
|
||||
},
|
||||
})
|
||||
: null,
|
||||
},
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
isIE,
|
||||
url: `https://${forwardedHost ?? host}${pathname}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const getTypebotFromPublicId = async (
|
||||
publicId?: string
|
||||
): Promise<TypebotPageProps['publishedTypebot'] | null> => {
|
||||
if (!publicId) return null
|
||||
const publishedTypebot = await prisma.publicTypebot.findFirst({
|
||||
where: { typebot: { publicId } },
|
||||
include: {
|
||||
typebot: { select: { name: true, isClosed: true, isArchived: true } },
|
||||
},
|
||||
})
|
||||
if (isNotDefined(publishedTypebot)) return null
|
||||
return omit(
|
||||
publishedTypebot,
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
) as TypebotPageProps['publishedTypebot']
|
||||
}
|
||||
|
||||
const getTypebotFromCustomDomain = async (
|
||||
customDomain: string
|
||||
): Promise<TypebotPageProps['publishedTypebot'] | null> => {
|
||||
const publishedTypebot = await prisma.publicTypebot.findFirst({
|
||||
where: { typebot: { customDomain } },
|
||||
include: {
|
||||
typebot: { select: { name: true, isClosed: true, isArchived: true } },
|
||||
},
|
||||
})
|
||||
if (isNotDefined(publishedTypebot)) return null
|
||||
return omit(
|
||||
publishedTypebot,
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
) as TypebotPageProps['publishedTypebot']
|
||||
}
|
||||
|
||||
const getHost = (
|
||||
req?: IncomingMessage
|
||||
): { host?: string; forwardedHost?: string } => ({
|
||||
host: req?.headers ? req.headers.host : window.location.host,
|
||||
forwardedHost: req?.headers['x-forwarded-host'] as string | undefined,
|
||||
})
|
||||
|
||||
const App = ({ publishedTypebot, ...props }: TypebotPageProps) => {
|
||||
if (!publishedTypebot || publishedTypebot.typebot.isArchived)
|
||||
return <NotFoundPage />
|
||||
if (publishedTypebot.typebot.isClosed)
|
||||
return <ErrorPage error={new Error('This bot is now closed')} />
|
||||
return <TypebotPage publishedTypebot={publishedTypebot} {...props} />
|
||||
}
|
||||
|
||||
export default App
|
15
apps/viewer/src/pages/_app.tsx
Normal file
15
apps/viewer/src/pages/_app.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import '../assets/styles.css'
|
||||
|
||||
type Props = {
|
||||
Component: React.ComponentType
|
||||
pageProps: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export default function MyApp({ Component, pageProps }: Props): JSX.Element {
|
||||
const { ...componentProps } = pageProps
|
||||
|
||||
return <Component {...componentProps} />
|
||||
}
|
16
apps/viewer/src/pages/_document.tsx
Normal file
16
apps/viewer/src/pages/_document.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
/* eslint-disable @next/next/no-sync-scripts */
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
const Document = () => (
|
||||
<Html>
|
||||
<Head>
|
||||
<script src="/__env.js" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
|
||||
export default Document
|
76
apps/viewer/src/pages/_error.tsx
Normal file
76
apps/viewer/src/pages/_error.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import NextErrorComponent from 'next/error'
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { NextPageContext } from 'next'
|
||||
|
||||
const MyError = ({
|
||||
statusCode,
|
||||
hasGetInitialPropsRun,
|
||||
err,
|
||||
}: {
|
||||
statusCode: number
|
||||
hasGetInitialPropsRun: boolean
|
||||
err: Error
|
||||
}) => {
|
||||
if (!hasGetInitialPropsRun && err) {
|
||||
// getInitialProps is not called in case of
|
||||
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
|
||||
// err via _app.js so it can be captured
|
||||
Sentry.captureException(err)
|
||||
// Flushing is not required in this case as it only happens on the client
|
||||
}
|
||||
|
||||
return <NextErrorComponent statusCode={statusCode} />
|
||||
}
|
||||
|
||||
MyError.getInitialProps = async (context: NextPageContext) => {
|
||||
const errorInitialProps = await NextErrorComponent.getInitialProps(context)
|
||||
|
||||
const { res, err, asPath } = context
|
||||
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
|
||||
// getInitialProps has run
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
errorInitialProps.hasGetInitialPropsRun = true
|
||||
|
||||
// Returning early because we don't want to log 404 errors to Sentry.
|
||||
if (res?.statusCode === 404) {
|
||||
return errorInitialProps
|
||||
}
|
||||
|
||||
// Running on the server, the response object (`res`) is available.
|
||||
//
|
||||
// Next.js will pass an err on the server if a page's data fetching methods
|
||||
// threw or returned a Promise that rejected
|
||||
//
|
||||
// Running on the client (browser), Next.js will provide an err if:
|
||||
//
|
||||
// - a page's `getInitialProps` threw or returned a Promise that rejected
|
||||
// - an exception was thrown somewhere in the React lifecycle (render,
|
||||
// componentDidMount, etc) that was caught by Next.js's React Error
|
||||
// Boundary. Read more about what types of exceptions are caught by Error
|
||||
// Boundaries: https://reactjs.org/docs/error-boundaries.html
|
||||
|
||||
if (err) {
|
||||
Sentry.captureException(err)
|
||||
|
||||
// Flushing before returning is necessary if deploying to Vercel, see
|
||||
// https://vercel.com/docs/platform/limits#streaming-responses
|
||||
await Sentry.flush(2000)
|
||||
|
||||
return errorInitialProps
|
||||
}
|
||||
|
||||
// If this point is reached, getInitialProps was called without any
|
||||
// information about what the error might be. This is unexpected and may
|
||||
// indicate a bug introduced in Next.js, so record it in Sentry
|
||||
Sentry.captureException(
|
||||
new Error(`_error.js getInitialProps missing data at path: ${asPath}`)
|
||||
)
|
||||
await Sentry.flush(2000)
|
||||
|
||||
return errorInitialProps
|
||||
}
|
||||
|
||||
export default MyError
|
@ -0,0 +1,128 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { badRequest, initMiddleware, methodNotAllowed } from 'utils/api'
|
||||
import { hasValue } from 'utils'
|
||||
import { GoogleSpreadsheet } from 'google-spreadsheet'
|
||||
import { Cell } from 'models'
|
||||
import Cors from 'cors'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { getAuthenticatedGoogleClient } from '@/lib/google-sheets'
|
||||
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
const resultId = req.query.resultId as string | undefined
|
||||
if (req.method === 'GET') {
|
||||
const spreadsheetId = req.query.spreadsheetId as string
|
||||
const sheetId = req.query.sheetId as string
|
||||
const credentialsId = req.query.credentialsId as string | undefined
|
||||
if (!hasValue(credentialsId)) return badRequest(res)
|
||||
const referenceCell = {
|
||||
column: req.query['referenceCell[column]'],
|
||||
value: req.query['referenceCell[value]'],
|
||||
} as Cell
|
||||
|
||||
const extractingColumns = getExtractingColumns(
|
||||
req.query.columns as string[] | string | undefined
|
||||
)
|
||||
if (!extractingColumns) return badRequest(res)
|
||||
const doc = new GoogleSpreadsheet(spreadsheetId)
|
||||
const client = await getAuthenticatedGoogleClient(credentialsId)
|
||||
if (!client)
|
||||
return res.status(404).send("Couldn't find credentials in database")
|
||||
doc.useOAuth2Client(client)
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[sheetId]
|
||||
try {
|
||||
const rows = await sheet.getRows()
|
||||
const row = rows.find(
|
||||
(row) => row[referenceCell.column as string] === referenceCell.value
|
||||
)
|
||||
if (!row) {
|
||||
await saveErrorLog(resultId, "Couldn't find reference cell")
|
||||
return res.status(404).send({ message: "Couldn't find row" })
|
||||
}
|
||||
const response = {
|
||||
...extractingColumns.reduce(
|
||||
(obj, column) => ({ ...obj, [column]: row[column] }),
|
||||
{}
|
||||
),
|
||||
}
|
||||
await saveSuccessLog(resultId, 'Succesfully fetched spreadsheet data')
|
||||
return res.send(response)
|
||||
} catch (err) {
|
||||
await saveErrorLog(resultId, "Couldn't fetch spreadsheet data", err)
|
||||
return res.status(500).send(err)
|
||||
}
|
||||
}
|
||||
if (req.method === 'POST') {
|
||||
const spreadsheetId = req.query.spreadsheetId as string
|
||||
const sheetId = req.query.sheetId as string
|
||||
const { credentialsId, values } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as {
|
||||
credentialsId?: string
|
||||
values: { [key: string]: string }
|
||||
}
|
||||
if (!hasValue(credentialsId)) return badRequest(res)
|
||||
const doc = new GoogleSpreadsheet(spreadsheetId)
|
||||
const auth = await getAuthenticatedGoogleClient(credentialsId)
|
||||
if (!auth)
|
||||
return res.status(404).send("Couldn't find credentials in database")
|
||||
doc.useOAuth2Client(auth)
|
||||
try {
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[sheetId]
|
||||
await sheet.addRow(values)
|
||||
await saveSuccessLog(resultId, 'Succesfully inserted row')
|
||||
return res.send({ message: 'Success' })
|
||||
} catch (err) {
|
||||
await saveErrorLog(resultId, "Couldn't fetch spreadsheet data", err)
|
||||
return res.status(500).send(err)
|
||||
}
|
||||
}
|
||||
if (req.method === 'PATCH') {
|
||||
const spreadsheetId = req.query.spreadsheetId as string
|
||||
const sheetId = req.query.sheetId as string
|
||||
const { credentialsId, values, referenceCell } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as {
|
||||
credentialsId?: string
|
||||
referenceCell: Cell
|
||||
values: { [key: string]: string }
|
||||
}
|
||||
if (!hasValue(credentialsId)) return badRequest(res)
|
||||
const doc = new GoogleSpreadsheet(spreadsheetId)
|
||||
const auth = await getAuthenticatedGoogleClient(credentialsId)
|
||||
if (!auth)
|
||||
return res.status(404).send("Couldn't find credentials in database")
|
||||
doc.useOAuth2Client(auth)
|
||||
try {
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[sheetId]
|
||||
const rows = await sheet.getRows()
|
||||
const updatingRowIndex = rows.findIndex(
|
||||
(row) => row[referenceCell.column as string] === referenceCell.value
|
||||
)
|
||||
if (updatingRowIndex === -1)
|
||||
return res.status(404).send({ message: "Couldn't find row to update" })
|
||||
for (const key in values) {
|
||||
rows[updatingRowIndex][key] = values[key]
|
||||
}
|
||||
await rows[updatingRowIndex].save()
|
||||
await saveSuccessLog(resultId, 'Succesfully updated row')
|
||||
return res.send({ message: 'Success' })
|
||||
} catch (err) {
|
||||
await saveErrorLog(resultId, "Couldn't fetch spreadsheet data", err)
|
||||
return res.status(500).send(err)
|
||||
}
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const getExtractingColumns = (columns: string | string[] | undefined) => {
|
||||
if (typeof columns === 'string') return [columns]
|
||||
if (Array.isArray(columns)) return columns
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,135 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import {
|
||||
badRequest,
|
||||
decrypt,
|
||||
forbidden,
|
||||
initMiddleware,
|
||||
methodNotAllowed,
|
||||
} from 'utils/api'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
import Cors from 'cors'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { PaymentInputOptions, StripeCredentialsData, Variable } from 'models'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { parseVariables } from 'bot-engine'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
|
||||
const currencySymbols: { [key: string]: string } = {
|
||||
USD: '$',
|
||||
EUR: '€',
|
||||
CRC: '₡',
|
||||
GBP: '£',
|
||||
ILS: '₪',
|
||||
INR: '₹',
|
||||
JPY: '¥',
|
||||
KRW: '₩',
|
||||
NGN: '₦',
|
||||
PHP: '₱',
|
||||
PLN: 'zł',
|
||||
PYG: '₲',
|
||||
THB: '฿',
|
||||
UAH: '₴',
|
||||
VND: '₫',
|
||||
}
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
if (req.method === 'POST') {
|
||||
const { inputOptions, isPreview, variables } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as {
|
||||
inputOptions: PaymentInputOptions
|
||||
isPreview: boolean
|
||||
variables: Variable[]
|
||||
}
|
||||
if (!inputOptions.credentialsId) return forbidden(res)
|
||||
const stripeKeys = await getStripeInfo(inputOptions.credentialsId)
|
||||
if (!stripeKeys) return forbidden(res)
|
||||
const stripe = new Stripe(
|
||||
isPreview && stripeKeys?.test?.secretKey
|
||||
? stripeKeys.test.secretKey
|
||||
: stripeKeys.live.secretKey,
|
||||
{ apiVersion: '2022-08-01' }
|
||||
)
|
||||
const amount =
|
||||
Number(parseVariables(variables)(inputOptions.amount)) *
|
||||
(isZeroDecimalCurrency(inputOptions.currency) ? 1 : 100)
|
||||
if (isNaN(amount)) return badRequest(res)
|
||||
// Create a PaymentIntent with the order amount and currency
|
||||
const receiptEmail = parseVariables(variables)(
|
||||
inputOptions.additionalInformation?.email
|
||||
)
|
||||
try {
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
amount,
|
||||
currency: inputOptions.currency,
|
||||
receipt_email: receiptEmail === '' ? undefined : receiptEmail,
|
||||
automatic_payment_methods: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
return res.send({
|
||||
clientSecret: paymentIntent.client_secret,
|
||||
publicKey:
|
||||
isPreview && stripeKeys.test?.publicKey
|
||||
? stripeKeys.test.publicKey
|
||||
: stripeKeys.live.publicKey,
|
||||
amountLabel: `${
|
||||
amount / (isZeroDecimalCurrency(inputOptions.currency) ? 1 : 100)
|
||||
}${
|
||||
currencySymbols[inputOptions.currency] ?? ` ${inputOptions.currency}`
|
||||
}`,
|
||||
})
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const error = err as any
|
||||
return 'raw' in error
|
||||
? res.status(error.raw.statusCode).send({
|
||||
error: {
|
||||
name: `${error.raw.type} ${error.raw.param}`,
|
||||
message: error.raw.message,
|
||||
},
|
||||
})
|
||||
: res.status(500).send({
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const getStripeInfo = async (
|
||||
credentialsId: string
|
||||
): Promise<StripeCredentialsData | undefined> => {
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: { id: credentialsId },
|
||||
})
|
||||
if (!credentials) return
|
||||
return decrypt(credentials.data, credentials.iv) as StripeCredentialsData
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/currencies#zero-decimal
|
||||
const isZeroDecimalCurrency = (currency: string) =>
|
||||
[
|
||||
'BIF',
|
||||
'CLP',
|
||||
'DJF',
|
||||
'GNF',
|
||||
'JPY',
|
||||
'KMF',
|
||||
'KRW',
|
||||
'MGA',
|
||||
'PYG',
|
||||
'RWF',
|
||||
'UGX',
|
||||
'VND',
|
||||
'VUV',
|
||||
'XAF',
|
||||
'XOF',
|
||||
'XPF',
|
||||
].includes(currency)
|
||||
|
||||
export default withSentry(handler)
|
15
apps/viewer/src/pages/api/mock/fail.ts
Normal file
15
apps/viewer/src/pages/api/mock/fail.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fail',
|
||||
})
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
15
apps/viewer/src/pages/api/mock/success.ts
Normal file
15
apps/viewer/src/pages/api/mock/success.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
statusMessage: 'OK',
|
||||
})
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
22
apps/viewer/src/pages/api/publicTypebots/[typebotId].ts
Normal file
22
apps/viewer/src/pages/api/publicTypebots/[typebotId].ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import Cors from 'cors'
|
||||
import { initMiddleware, methodNotAllowed, notFound } from 'utils/api'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const typebot = await prisma.publicTypebot.findUnique({
|
||||
where: { typebotId },
|
||||
})
|
||||
if (!typebot) return notFound(res)
|
||||
return res.send({ typebot })
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
20
apps/viewer/src/pages/api/typebots.ts
Normal file
20
apps/viewer/src/pages/api/typebots.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
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: { workspace: { members: { some: { userId: user.id } } } },
|
||||
select: { name: true, publishedTypebotId: true, id: true },
|
||||
})
|
||||
return res.send({ typebots })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,232 @@
|
||||
import {
|
||||
defaultWebhookAttributes,
|
||||
KeyValue,
|
||||
PublicTypebot,
|
||||
ResultValues,
|
||||
Typebot,
|
||||
Variable,
|
||||
Webhook,
|
||||
WebhookOptions,
|
||||
WebhookResponse,
|
||||
WebhookBlock,
|
||||
HttpMethod,
|
||||
} from 'models'
|
||||
import { parseVariables } from 'bot-engine'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import got, { Method, Headers, HTTPError } from 'got'
|
||||
import { byId, omit, parseAnswers } from 'utils'
|
||||
import { initMiddleware, methodNotAllowed, notFound } from 'utils/api'
|
||||
import { stringify } from 'qs'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import Cors from 'cors'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getLinkedTypebots } from '@/features/typebotLink/api'
|
||||
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
|
||||
import { parseSampleResult } from '@/features/webhook/api'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const blockId = req.query.blockId as string
|
||||
const resultId = req.query.resultId as string | undefined
|
||||
const { resultValues, variables } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as {
|
||||
resultValues: ResultValues | undefined
|
||||
variables: Variable[]
|
||||
}
|
||||
const typebot = (await prisma.typebot.findUnique({
|
||||
where: { id: typebotId },
|
||||
include: { webhooks: true },
|
||||
})) as unknown as (Typebot & { webhooks: Webhook[] }) | null
|
||||
if (!typebot) return notFound(res)
|
||||
const block = typebot.groups
|
||||
.flatMap((g) => g.blocks)
|
||||
.find(byId(blockId)) as WebhookBlock
|
||||
const webhook = typebot.webhooks.find(byId(block.webhookId))
|
||||
if (!webhook)
|
||||
return res
|
||||
.status(404)
|
||||
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
|
||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||
const result = await executeWebhook(typebot)(
|
||||
preparedWebhook,
|
||||
variables,
|
||||
block.groupId,
|
||||
resultValues,
|
||||
resultId
|
||||
)
|
||||
return res.status(200).send(result)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const prepareWebhookAttributes = (
|
||||
webhook: Webhook,
|
||||
options: WebhookOptions
|
||||
): Webhook => {
|
||||
if (options.isAdvancedConfig === false) {
|
||||
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
|
||||
} else if (options.isCustomBody === false) {
|
||||
return { ...webhook, body: '{{state}}' }
|
||||
}
|
||||
return webhook
|
||||
}
|
||||
|
||||
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
||||
|
||||
export const executeWebhook =
|
||||
(typebot: Typebot) =>
|
||||
async (
|
||||
webhook: Webhook,
|
||||
variables: Variable[],
|
||||
groupId: string,
|
||||
resultValues?: ResultValues,
|
||||
resultId?: string
|
||||
): Promise<WebhookResponse> => {
|
||||
if (!webhook.url || !webhook.method)
|
||||
return {
|
||||
statusCode: 400,
|
||||
data: { message: `Webhook doesn't have url or method` },
|
||||
}
|
||||
const basicAuth: { username?: string; password?: string } = {}
|
||||
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
||||
(h) =>
|
||||
h.key?.toLowerCase() === 'authorization' &&
|
||||
h.value?.toLowerCase()?.includes('basic')
|
||||
)
|
||||
const isUsernamePasswordBasicAuth =
|
||||
basicAuthHeaderIdx !== -1 &&
|
||||
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
|
||||
if (isUsernamePasswordBasicAuth) {
|
||||
const [username, password] =
|
||||
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
|
||||
basicAuth.username = username
|
||||
basicAuth.password = password
|
||||
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
||||
}
|
||||
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
|
||||
| Headers
|
||||
| undefined
|
||||
const queryParams = stringify(
|
||||
convertKeyValueTableToObject(webhook.queryParams, variables)
|
||||
)
|
||||
const contentType = headers ? headers['Content-Type'] : undefined
|
||||
const linkedTypebots = await getLinkedTypebots(typebot)
|
||||
const bodyContent = await getBodyContent(
|
||||
typebot,
|
||||
linkedTypebots
|
||||
)({
|
||||
body: webhook.body,
|
||||
resultValues,
|
||||
groupId,
|
||||
})
|
||||
const { data: body, isJson } =
|
||||
bodyContent && webhook.method !== HttpMethod.GET
|
||||
? safeJsonParse(
|
||||
parseVariables(variables, {
|
||||
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
|
||||
})(bodyContent)
|
||||
)
|
||||
: { data: undefined, isJson: false }
|
||||
|
||||
const request = {
|
||||
url: parseVariables(variables)(
|
||||
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
||||
),
|
||||
method: webhook.method as Method,
|
||||
headers,
|
||||
...basicAuth,
|
||||
json:
|
||||
contentType !== 'x-www-form-urlencoded' && body && isJson
|
||||
? body
|
||||
: undefined,
|
||||
form: contentType === 'x-www-form-urlencoded' && body ? body : undefined,
|
||||
body: body && !isJson ? body : undefined,
|
||||
}
|
||||
try {
|
||||
const response = await got(request.url, omit(request, 'url'))
|
||||
await saveSuccessLog(resultId, 'Webhook successfuly executed.', {
|
||||
statusCode: response.statusCode,
|
||||
request,
|
||||
response: safeJsonParse(response.body).data,
|
||||
})
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
data: safeJsonParse(response.body).data,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
const response = {
|
||||
statusCode: error.response.statusCode,
|
||||
data: safeJsonParse(error.response.body as string).data,
|
||||
}
|
||||
await saveErrorLog(resultId, 'Webhook returned an error', {
|
||||
request,
|
||||
response,
|
||||
})
|
||||
return response
|
||||
}
|
||||
const response = {
|
||||
statusCode: 500,
|
||||
data: { message: `Error from Typebot server: ${error}` },
|
||||
}
|
||||
console.error(error)
|
||||
await saveErrorLog(resultId, 'Webhook failed to execute', {
|
||||
request,
|
||||
response,
|
||||
})
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
const getBodyContent =
|
||||
(
|
||||
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
||||
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||
) =>
|
||||
async ({
|
||||
body,
|
||||
resultValues,
|
||||
groupId,
|
||||
}: {
|
||||
body?: string | null
|
||||
resultValues?: ResultValues
|
||||
groupId: string
|
||||
}): Promise<string | undefined> => {
|
||||
if (!body) return
|
||||
return body === '{{state}}'
|
||||
? JSON.stringify(
|
||||
resultValues
|
||||
? parseAnswers(typebot, linkedTypebots)(resultValues)
|
||||
: await parseSampleResult(typebot, linkedTypebots)(groupId)
|
||||
)
|
||||
: body
|
||||
}
|
||||
|
||||
const convertKeyValueTableToObject = (
|
||||
keyValues: KeyValue[] | undefined,
|
||||
variables: Variable[]
|
||||
) => {
|
||||
if (!keyValues) return
|
||||
return keyValues.reduce((object, item) => {
|
||||
if (!item.key) return {}
|
||||
return {
|
||||
...object,
|
||||
[item.key]: parseVariables(variables)(item.value ?? ''),
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const safeJsonParse = (json: string): { data: any; isJson: boolean } => {
|
||||
try {
|
||||
return { data: JSON.parse(json), isJson: true }
|
||||
} catch (err) {
|
||||
return { data: json, isJson: false }
|
||||
}
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,34 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import { getLinkedTypebots } from '@/features/typebotLink/api'
|
||||
import { parseSampleResult } from '@/features/webhook/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Typebot } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await authenticateUser(req)
|
||||
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const blockId = req.query.blockId as string
|
||||
const typebot = (await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
})) as unknown as Typebot | undefined
|
||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||
const block = typebot.groups
|
||||
.flatMap((g) => g.blocks)
|
||||
.find((s) => s.id === blockId)
|
||||
if (!block) return res.status(404).send({ message: 'Group not found' })
|
||||
const linkedTypebots = await getLinkedTypebots(typebot, user)
|
||||
return res.send(
|
||||
await parseSampleResult(typebot, linkedTypebots)(block.groupId)
|
||||
)
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
@ -0,0 +1,70 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import {
|
||||
defaultWebhookAttributes,
|
||||
ResultValues,
|
||||
Typebot,
|
||||
Variable,
|
||||
Webhook,
|
||||
WebhookOptions,
|
||||
WebhookBlock,
|
||||
} from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { initMiddleware, methodNotAllowed, notFound } from 'utils/api'
|
||||
import { byId } from 'utils'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import Cors from 'cors'
|
||||
import { executeWebhook } from '../../executeWebhook'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const groupId = req.query.groupId as string
|
||||
const blockId = req.query.blockId as string
|
||||
const resultId = req.query.resultId as string | undefined
|
||||
const { resultValues, variables } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as {
|
||||
resultValues: ResultValues | undefined
|
||||
variables: Variable[]
|
||||
}
|
||||
const typebot = (await prisma.typebot.findUnique({
|
||||
where: { id: typebotId },
|
||||
include: { webhooks: true },
|
||||
})) as unknown as (Typebot & { webhooks: Webhook[] }) | null
|
||||
if (!typebot) return notFound(res)
|
||||
const block = typebot.groups
|
||||
.find(byId(groupId))
|
||||
?.blocks.find(byId(blockId)) as WebhookBlock
|
||||
const webhook = typebot.webhooks.find(byId(block.webhookId))
|
||||
if (!webhook)
|
||||
return res
|
||||
.status(404)
|
||||
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
|
||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||
const result = await executeWebhook(typebot)(
|
||||
preparedWebhook,
|
||||
variables,
|
||||
groupId,
|
||||
resultValues,
|
||||
resultId
|
||||
)
|
||||
return res.status(200).send(result)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const prepareWebhookAttributes = (
|
||||
webhook: Webhook,
|
||||
options: WebhookOptions
|
||||
): Webhook => {
|
||||
if (options.isAdvancedConfig === false) {
|
||||
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
|
||||
} else if (options.isCustomBody === false) {
|
||||
return { ...webhook, body: '{{state}}' }
|
||||
}
|
||||
return webhook
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,28 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import { getLinkedTypebots } from '@/features/typebotLink/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Typebot } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
import { parseSampleResult } from '@/features/webhook/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await authenticateUser(req)
|
||||
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const groupId = req.query.groupId as string
|
||||
const typebot = (await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
})) as unknown as Typebot | undefined
|
||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||
const linkedTypebots = await getLinkedTypebots(typebot, user)
|
||||
return res.send(await parseSampleResult(typebot, linkedTypebots)(groupId))
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
@ -0,0 +1,47 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { Typebot, WebhookBlock } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
import { byId } from 'utils'
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await authenticateUser(req)
|
||||
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||
if (req.method === 'POST') {
|
||||
const body = req.body as Record<string, string>
|
||||
if (!('url' in body))
|
||||
return res.status(403).send({ message: 'url is missing in body' })
|
||||
const { url } = body
|
||||
const typebotId = req.query.typebotId as string
|
||||
const groupId = req.query.groupId as string
|
||||
const blockId = req.query.blockId as string
|
||||
const typebot = (await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
})) as unknown as Typebot | undefined
|
||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||
try {
|
||||
const { webhookId } = typebot.groups
|
||||
.find(byId(groupId))
|
||||
?.blocks.find(byId(blockId)) as WebhookBlock
|
||||
await prisma.webhook.upsert({
|
||||
where: { id: webhookId },
|
||||
update: { url, body: '{{state}}', method: 'POST' },
|
||||
create: { url, body: '{{state}}', method: 'POST', typebotId },
|
||||
})
|
||||
|
||||
return res.send({ message: 'success' })
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({ message: "blockId doesn't point to a Webhook block" })
|
||||
}
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,42 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { Typebot, WebhookBlock } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
import { byId } from 'utils'
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await authenticateUser(req)
|
||||
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const groupId = req.query.groupId as string
|
||||
const blockId = req.query.blockId as string
|
||||
const typebot = (await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
})) as unknown as Typebot | undefined
|
||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||
try {
|
||||
const { webhookId } = typebot.groups
|
||||
.find(byId(groupId))
|
||||
?.blocks.find(byId(blockId)) as WebhookBlock
|
||||
await prisma.webhook.update({
|
||||
where: { id: webhookId },
|
||||
data: { url: null },
|
||||
})
|
||||
|
||||
return res.send({ message: 'success' })
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({ message: "blockId doesn't point to a Webhook block" })
|
||||
}
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,168 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { WorkspaceRole } from 'db'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { InputBlockType, PublicTypebot } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api'
|
||||
import { byId, getStorageLimit, isDefined, env } from 'utils'
|
||||
import {
|
||||
sendAlmostReachedStorageLimitEmail,
|
||||
sendReachedStorageLimitEmail,
|
||||
} from 'emails'
|
||||
|
||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||
|
||||
const handler = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<void> => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
if (req.method === 'GET') {
|
||||
if (
|
||||
!process.env.S3_ENDPOINT ||
|
||||
!process.env.S3_ACCESS_KEY ||
|
||||
!process.env.S3_SECRET_KEY
|
||||
)
|
||||
return badRequest(
|
||||
res,
|
||||
'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY'
|
||||
)
|
||||
const filePath = req.query.filePath as string | undefined
|
||||
const fileType = req.query.fileType as string | undefined
|
||||
const typebotId = req.query.typebotId as string
|
||||
const blockId = req.query.blockId as string
|
||||
if (!filePath) return badRequest(res, 'Missing filePath or fileType')
|
||||
// const hasReachedStorageLimit = await checkStorageLimit(typebotId)
|
||||
const typebot = (await prisma.publicTypebot.findFirst({
|
||||
where: { typebotId },
|
||||
})) as unknown as PublicTypebot
|
||||
const fileUploadBlock = typebot.groups
|
||||
.flatMap((g) => g.blocks)
|
||||
.find(byId(blockId))
|
||||
if (fileUploadBlock?.type !== InputBlockType.FILE)
|
||||
return badRequest(res, 'Not a file upload block')
|
||||
const sizeLimit = fileUploadBlock.options.sizeLimit
|
||||
? Math.min(fileUploadBlock.options.sizeLimit, 500)
|
||||
: 10
|
||||
|
||||
const presignedUrl = generatePresignedUrl({
|
||||
fileType,
|
||||
filePath,
|
||||
sizeLimit: sizeLimit * 1024 * 1024,
|
||||
})
|
||||
|
||||
// TODO: enable storage limit on 1st of November 2022
|
||||
return res.status(200).send({ presignedUrl, hasReachedStorageLimit: false })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const checkStorageLimit = async (typebotId: string) => {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: { id: typebotId },
|
||||
include: {
|
||||
workspace: {
|
||||
select: {
|
||||
id: true,
|
||||
additionalStorageIndex: true,
|
||||
plan: true,
|
||||
storageLimitFirstEmailSentAt: true,
|
||||
storageLimitSecondEmailSentAt: true,
|
||||
customStorageLimit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot?.workspace) throw new Error('Workspace not found')
|
||||
const { workspace } = typebot
|
||||
const {
|
||||
_sum: { storageUsed: totalStorageUsed },
|
||||
} = await prisma.answer.aggregate({
|
||||
where: {
|
||||
storageUsed: { gt: 0 },
|
||||
result: {
|
||||
typebot: {
|
||||
workspace: {
|
||||
id: typebot?.workspaceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_sum: { storageUsed: true },
|
||||
})
|
||||
if (!totalStorageUsed) return false
|
||||
const hasSentFirstEmail = workspace.storageLimitFirstEmailSentAt !== null
|
||||
const hasSentSecondEmail = workspace.storageLimitSecondEmailSentAt !== null
|
||||
const storageLimit = getStorageLimit(typebot.workspace)
|
||||
const storageLimitBytes = storageLimit * 1024 * 1024 * 1024
|
||||
if (
|
||||
totalStorageUsed >= storageLimitBytes * LIMIT_EMAIL_TRIGGER_PERCENT &&
|
||||
!hasSentFirstEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendAlmostReachStorageLimitNotification({
|
||||
workspaceId: workspace.id,
|
||||
storageLimit,
|
||||
})
|
||||
if (
|
||||
totalStorageUsed >= storageLimitBytes &&
|
||||
!hasSentSecondEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendReachStorageLimitNotification({
|
||||
workspaceId: workspace.id,
|
||||
storageLimit,
|
||||
})
|
||||
return totalStorageUsed >= storageLimitBytes
|
||||
}
|
||||
|
||||
const sendAlmostReachStorageLimitNotification = async ({
|
||||
workspaceId,
|
||||
storageLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
storageLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
|
||||
await sendAlmostReachedStorageLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
storageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { storageLimitFirstEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
const sendReachStorageLimitNotification = async ({
|
||||
workspaceId,
|
||||
storageLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
storageLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
|
||||
await sendReachedStorageLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
storageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { storageLimitSecondEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,46 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { Typebot, WebhookBlock } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { byId } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await authenticateUser(req)
|
||||
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||
if (req.method === 'POST') {
|
||||
const body = req.body as Record<string, string>
|
||||
if (!('url' in body))
|
||||
return res.status(403).send({ message: 'url is missing in body' })
|
||||
const { url } = body
|
||||
const typebotId = req.query.typebotId as string
|
||||
const blockId = req.query.blockId as string
|
||||
const typebot = (await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
})) as unknown as Typebot | undefined
|
||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||
try {
|
||||
const { webhookId } = typebot.groups
|
||||
.flatMap((g) => g.blocks)
|
||||
.find(byId(blockId)) as WebhookBlock
|
||||
await prisma.webhook.upsert({
|
||||
where: { id: webhookId },
|
||||
update: { url, body: '{{state}}', method: 'POST' },
|
||||
create: { url, body: '{{state}}', method: 'POST', typebotId },
|
||||
})
|
||||
|
||||
return res.send({ message: 'success' })
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({ message: "groupId doesn't point to a Webhook block" })
|
||||
}
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,41 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { Typebot, WebhookBlock } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { byId } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await authenticateUser(req)
|
||||
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const blockId = req.query.blockId as string
|
||||
const typebot = (await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
})) as unknown as Typebot | undefined
|
||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||
try {
|
||||
const { webhookId } = typebot.groups
|
||||
.flatMap((g) => g.blocks)
|
||||
.find(byId(blockId)) as WebhookBlock
|
||||
await prisma.webhook.update({
|
||||
where: { id: webhookId },
|
||||
data: { url: null },
|
||||
})
|
||||
|
||||
return res.send({ message: 'success' })
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({ message: "groupId doesn't point to a Webhook block" })
|
||||
}
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,207 @@
|
||||
import {
|
||||
PublicTypebot,
|
||||
ResultValues,
|
||||
SendEmailOptions,
|
||||
SmtpCredentialsData,
|
||||
} from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { createTransport, getTestMessageUrl } from 'nodemailer'
|
||||
import { isEmpty, isNotDefined, omit, parseAnswers } from 'utils'
|
||||
import { methodNotAllowed, initMiddleware, decrypt } from 'utils/api'
|
||||
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
|
||||
|
||||
import Cors from 'cors'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import Mail from 'nodemailer/lib/mailer'
|
||||
import { DefaultBotNotificationEmail, render } from 'emails'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getLinkedTypebots } from '@/features/typebotLink/api'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
|
||||
const defaultTransportOptions = {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: process.env.SMTP_SECURE ? process.env.SMTP_SECURE === 'true' : false,
|
||||
auth: {
|
||||
user: process.env.SMTP_USERNAME,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
},
|
||||
}
|
||||
|
||||
const defaultFrom = {
|
||||
name: process.env.SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
|
||||
email: process.env.SMTP_FROM?.match(/\<(.*)\>/)?.pop(),
|
||||
}
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const resultId = req.query.resultId as string | undefined
|
||||
const {
|
||||
credentialsId,
|
||||
recipients,
|
||||
body,
|
||||
subject,
|
||||
cc,
|
||||
bcc,
|
||||
replyTo,
|
||||
isBodyCode,
|
||||
isCustomBody,
|
||||
resultValues,
|
||||
fileUrls,
|
||||
} = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as SendEmailOptions & {
|
||||
resultValues: ResultValues
|
||||
fileUrls?: string
|
||||
}
|
||||
const { name: replyToName } = parseEmailRecipient(replyTo)
|
||||
|
||||
const { host, port, isTlsEnabled, username, password, from } =
|
||||
(await getEmailInfo(credentialsId)) ?? {}
|
||||
if (!from)
|
||||
return res.status(404).send({ message: "Couldn't find credentials" })
|
||||
|
||||
const transportConfig = {
|
||||
host,
|
||||
port,
|
||||
secure: isTlsEnabled ?? undefined,
|
||||
auth: {
|
||||
user: username,
|
||||
pass: password,
|
||||
},
|
||||
}
|
||||
|
||||
const emailBody = await getEmailBody({
|
||||
body,
|
||||
isCustomBody,
|
||||
isBodyCode,
|
||||
typebotId,
|
||||
resultValues,
|
||||
})
|
||||
|
||||
if (!emailBody) {
|
||||
await saveErrorLog(resultId, 'Email not sent', {
|
||||
transportConfig,
|
||||
recipients,
|
||||
subject,
|
||||
cc,
|
||||
bcc,
|
||||
replyTo,
|
||||
emailBody,
|
||||
})
|
||||
return res.status(404).send({ message: "Couldn't find email body" })
|
||||
}
|
||||
const transporter = createTransport(transportConfig)
|
||||
const email: Mail.Options = {
|
||||
from: `"${isEmpty(replyToName) ? from.name : replyToName}" <${
|
||||
from.email
|
||||
}>`,
|
||||
cc,
|
||||
bcc,
|
||||
to: recipients,
|
||||
replyTo,
|
||||
subject,
|
||||
attachments: fileUrls?.split(', ').map((url) => ({ path: url })),
|
||||
...emailBody,
|
||||
}
|
||||
try {
|
||||
const info = await transporter.sendMail(email)
|
||||
await saveSuccessLog(resultId, 'Email successfully sent', {
|
||||
transportConfig: {
|
||||
...transportConfig,
|
||||
auth: { user: transportConfig.auth.user, pass: '******' },
|
||||
},
|
||||
email,
|
||||
})
|
||||
return res.status(200).send({
|
||||
message: 'Email sent!',
|
||||
info,
|
||||
previewUrl: getTestMessageUrl(info),
|
||||
})
|
||||
} catch (err) {
|
||||
await saveErrorLog(resultId, 'Email not sent', {
|
||||
transportConfig: {
|
||||
...transportConfig,
|
||||
auth: { user: transportConfig.auth.user, pass: '******' },
|
||||
},
|
||||
email,
|
||||
error: err,
|
||||
})
|
||||
return res.status(500).send({
|
||||
message: `Email not sent. Error: ${err}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const getEmailInfo = async (
|
||||
credentialsId: string
|
||||
): Promise<SmtpCredentialsData | undefined> => {
|
||||
if (credentialsId === 'default')
|
||||
return {
|
||||
host: defaultTransportOptions.host,
|
||||
port: defaultTransportOptions.port,
|
||||
username: defaultTransportOptions.auth.user,
|
||||
password: defaultTransportOptions.auth.pass,
|
||||
isTlsEnabled: undefined,
|
||||
from: defaultFrom,
|
||||
}
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: { id: credentialsId },
|
||||
})
|
||||
if (!credentials) return
|
||||
return decrypt(credentials.data, credentials.iv) as SmtpCredentialsData
|
||||
}
|
||||
|
||||
const getEmailBody = async ({
|
||||
body,
|
||||
isCustomBody,
|
||||
isBodyCode,
|
||||
typebotId,
|
||||
resultValues,
|
||||
}: { typebotId: string; resultValues: ResultValues } & Pick<
|
||||
SendEmailOptions,
|
||||
'isCustomBody' | 'isBodyCode' | 'body'
|
||||
>): Promise<{ html?: string; text?: string } | undefined> => {
|
||||
if (isCustomBody || (isNotDefined(isCustomBody) && !isEmpty(body)))
|
||||
return {
|
||||
html: isBodyCode ? body : undefined,
|
||||
text: !isBodyCode ? body : undefined,
|
||||
}
|
||||
const typebot = (await prisma.publicTypebot.findUnique({
|
||||
where: { typebotId },
|
||||
})) as unknown as PublicTypebot
|
||||
if (!typebot) return
|
||||
const linkedTypebots = await getLinkedTypebots(typebot)
|
||||
const answers = parseAnswers(typebot, linkedTypebots)(resultValues)
|
||||
return {
|
||||
html: render(
|
||||
<DefaultBotNotificationEmail
|
||||
resultsUrl={`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
|
||||
answers={omit(answers, 'submittedAt')}
|
||||
/>
|
||||
).html,
|
||||
}
|
||||
}
|
||||
|
||||
const parseEmailRecipient = (
|
||||
recipient?: string
|
||||
): { email?: string; name?: string } => {
|
||||
if (!recipient) return {}
|
||||
if (recipient.includes('<')) {
|
||||
const [name, email] = recipient.split('<')
|
||||
return {
|
||||
name: name.replace(/>/g, '').trim().replace(/"/g, ''),
|
||||
email: email.replace('>', '').trim(),
|
||||
}
|
||||
}
|
||||
return {
|
||||
email: recipient,
|
||||
}
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
168
apps/viewer/src/pages/api/typebots/[typebotId]/results.ts
Normal file
168
apps/viewer/src/pages/api/typebots/[typebotId]/results.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Workspace, WorkspaceRole } from 'db'
|
||||
import {
|
||||
sendAlmostReachedChatsLimitEmail,
|
||||
sendReachedChatsLimitEmail,
|
||||
} from 'emails'
|
||||
import { ResultWithAnswers } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { env, getChatsLimit, isDefined } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||
|
||||
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 as string
|
||||
const limit = Number(req.query.limit)
|
||||
const results = (await prisma.result.findMany({
|
||||
where: {
|
||||
typebot: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
include: { answers: true },
|
||||
})) as unknown as ResultWithAnswers[]
|
||||
return res.send({ results })
|
||||
}
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const result = await prisma.result.create({
|
||||
data: {
|
||||
typebotId,
|
||||
isCompleted: false,
|
||||
},
|
||||
include: {
|
||||
typebot: {
|
||||
include: {
|
||||
workspace: {
|
||||
select: {
|
||||
id: true,
|
||||
plan: true,
|
||||
additionalChatsIndex: true,
|
||||
chatsLimitFirstEmailSentAt: true,
|
||||
chatsLimitSecondEmailSentAt: true,
|
||||
customChatsLimit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// TODO: enable storage limit on 1st of November 2022
|
||||
// const hasReachedLimit = await checkChatsUsage(result.typebot.workspace)
|
||||
res.send({ result, hasReachedLimit: false })
|
||||
return
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const checkChatsUsage = async (
|
||||
workspace: Pick<
|
||||
Workspace,
|
||||
| 'id'
|
||||
| 'plan'
|
||||
| 'additionalChatsIndex'
|
||||
| 'chatsLimitFirstEmailSentAt'
|
||||
| 'chatsLimitSecondEmailSentAt'
|
||||
| 'customChatsLimit'
|
||||
>
|
||||
) => {
|
||||
const chatsLimit = getChatsLimit(workspace)
|
||||
if (chatsLimit === -1) return
|
||||
const now = new Date()
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
||||
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
|
||||
const chatsCount = await prisma.result.count({
|
||||
where: {
|
||||
typebot: { workspaceId: workspace.id },
|
||||
hasStarted: true,
|
||||
createdAt: { gte: firstDayOfMonth, lte: lastDayOfMonth },
|
||||
},
|
||||
})
|
||||
const hasSentFirstEmail =
|
||||
workspace.chatsLimitFirstEmailSentAt !== null &&
|
||||
workspace.chatsLimitFirstEmailSentAt < firstDayOfNextMonth &&
|
||||
workspace.chatsLimitFirstEmailSentAt > firstDayOfMonth
|
||||
const hasSentSecondEmail =
|
||||
workspace.chatsLimitSecondEmailSentAt !== null &&
|
||||
workspace.chatsLimitSecondEmailSentAt < firstDayOfNextMonth &&
|
||||
workspace.chatsLimitSecondEmailSentAt > firstDayOfMonth
|
||||
if (
|
||||
chatsCount >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
|
||||
!hasSentFirstEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendAlmostReachChatsLimitNotification({
|
||||
workspaceId: workspace.id,
|
||||
chatsLimit,
|
||||
})
|
||||
if (
|
||||
chatsCount >= chatsLimit &&
|
||||
!hasSentSecondEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendReachedAlertNotification({
|
||||
workspaceId: workspace.id,
|
||||
chatsLimit,
|
||||
})
|
||||
return chatsCount >= chatsLimit
|
||||
}
|
||||
|
||||
const sendAlmostReachChatsLimitNotification = async ({
|
||||
workspaceId,
|
||||
chatsLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
chatsLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
|
||||
await sendAlmostReachedChatsLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
chatsLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { chatsLimitFirstEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
const sendReachedAlertNotification = async ({
|
||||
workspaceId,
|
||||
chatsLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
chatsLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
|
||||
await sendReachedChatsLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
chatsLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { chatsLimitSecondEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
export default handler
|
@ -0,0 +1,22 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Result } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'PATCH') {
|
||||
const data = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as Result
|
||||
const resultId = req.query.resultId as string
|
||||
const result = await prisma.result.update({
|
||||
where: { id: resultId },
|
||||
data,
|
||||
})
|
||||
return res.send(result)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,43 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { Answer } from 'db'
|
||||
import { got } from 'got'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'PUT') {
|
||||
const { uploadedFiles, ...answer } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as Answer & { uploadedFiles?: boolean }
|
||||
let storageUsed = 0
|
||||
if (uploadedFiles && answer.content.includes('http')) {
|
||||
const fileUrls = answer.content.split(', ')
|
||||
const hasReachedStorageLimit = fileUrls[0] === null
|
||||
if (!hasReachedStorageLimit) {
|
||||
for (const url of fileUrls) {
|
||||
const { headers } = await got(url)
|
||||
const size = headers['content-length']
|
||||
if (isNotDefined(size)) return
|
||||
storageUsed += parseInt(size, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await prisma.answer.upsert({
|
||||
where: {
|
||||
resultId_blockId_groupId: {
|
||||
resultId: answer.resultId,
|
||||
groupId: answer.groupId,
|
||||
blockId: answer.blockId,
|
||||
},
|
||||
},
|
||||
create: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
|
||||
update: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
|
||||
})
|
||||
return res.send(result)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,41 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { Group, WebhookBlock } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { byId, isWebhookBlock } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
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 as string
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
select: { groups: true, webhooks: true },
|
||||
})
|
||||
const emptyWebhookBlocks = (typebot?.groups as Group[]).reduce<
|
||||
{ blockId: string; name: string; url: string | undefined }[]
|
||||
>((emptyWebhookBlocks, group) => {
|
||||
const blocks = group.blocks.filter((block) =>
|
||||
isWebhookBlock(block)
|
||||
) as WebhookBlock[]
|
||||
return [
|
||||
...emptyWebhookBlocks,
|
||||
...blocks.map((b) => ({
|
||||
blockId: b.id,
|
||||
name: `${group.title} > ${b.id}`,
|
||||
url: typebot?.webhooks.find(byId(b.webhookId))?.url ?? undefined,
|
||||
})),
|
||||
]
|
||||
}, [])
|
||||
return res.send({ blocks: emptyWebhookBlocks })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,45 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { Group, WebhookBlock } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { byId, isNotDefined, isWebhookBlock } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
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 as string
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
select: { groups: true, webhooks: true },
|
||||
})
|
||||
const emptyWebhookBlocks = (typebot?.groups as Group[]).reduce<
|
||||
{ groupId: string; id: string; name: string }[]
|
||||
>((emptyWebhookBlocks, group) => {
|
||||
const blocks = group.blocks.filter(
|
||||
(block) =>
|
||||
isWebhookBlock(block) &&
|
||||
isNotDefined(
|
||||
typebot?.webhooks.find(byId((block as WebhookBlock).webhookId))?.url
|
||||
)
|
||||
)
|
||||
return [
|
||||
...emptyWebhookBlocks,
|
||||
...blocks.map((s) => ({
|
||||
id: s.id,
|
||||
groupId: s.groupId,
|
||||
name: `${group.title} > ${s.id}`,
|
||||
})),
|
||||
]
|
||||
}, [])
|
||||
return res.send({ steps: emptyWebhookBlocks })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
17
apps/viewer/src/pages/api/users/me.ts
Normal file
17
apps/viewer/src/pages/api/users/me.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { authenticateUser } from '@/features/auth/api'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
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, email: user.email })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
Reference in New Issue
Block a user