2
0

♻️ (viewer) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 10:28:03 +01:00
committed by Baptiste Arnaud
parent 643571fe7d
commit a9d04798bc
80 changed files with 523 additions and 491 deletions

View 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

View 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} />
}

View 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

View 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

View File

@ -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)

View File

@ -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)

View 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)

View 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)

View 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)

View 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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View 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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View 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)