2
0

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

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@ -0,0 +1,197 @@
import NextAuth, { Account } from 'next-auth'
import EmailProvider from 'next-auth/providers/email'
import GitHubProvider from 'next-auth/providers/github'
import GitlabProvider from 'next-auth/providers/gitlab'
import GoogleProvider from 'next-auth/providers/google'
import FacebookProvider from 'next-auth/providers/facebook'
import AzureADProvider from 'next-auth/providers/azure-ad'
import prisma from '@/lib/prisma'
import { Provider } from 'next-auth/providers'
import { NextApiRequest, NextApiResponse } from 'next'
import { withSentry } from '@sentry/nextjs'
import { CustomAdapter } from './adapter'
import { User } from 'db'
import { env, isNotEmpty } from 'utils'
import { mockedUser } from '@/features/auth'
const providers: Provider[] = []
if (
isNotEmpty(process.env.GITHUB_CLIENT_ID) &&
isNotEmpty(process.env.GITHUB_CLIENT_SECRET)
)
providers.push(
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
})
)
if (isNotEmpty(env('SMTP_FROM')) && process.env.SMTP_AUTH_DISABLED !== 'true')
providers.push(
EmailProvider({
server: {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 25,
secure: process.env.SMTP_SECURE
? process.env.SMTP_SECURE === 'true'
: false,
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
},
},
from: env('SMTP_FROM'),
})
)
if (
isNotEmpty(process.env.GOOGLE_CLIENT_ID) &&
isNotEmpty(process.env.GOOGLE_CLIENT_SECRET)
)
providers.push(
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
})
)
if (
isNotEmpty(process.env.FACEBOOK_CLIENT_ID) &&
isNotEmpty(process.env.FACEBOOK_CLIENT_SECRET)
)
providers.push(
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
})
)
if (
isNotEmpty(process.env.GITLAB_CLIENT_ID) &&
isNotEmpty(process.env.GITLAB_CLIENT_SECRET)
) {
const BASE_URL = process.env.GITLAB_BASE_URL || 'https://gitlab.com'
providers.push(
GitlabProvider({
clientId: process.env.GITLAB_CLIENT_ID,
clientSecret: process.env.GITLAB_CLIENT_SECRET,
authorization: `${BASE_URL}/oauth/authorize?scope=read_api`,
token: `${BASE_URL}/oauth/token`,
userinfo: `${BASE_URL}/api/v4/user`,
name: process.env.GITLAB_NAME || 'GitLab',
})
)
}
if (
isNotEmpty(process.env.AZURE_AD_CLIENT_ID) &&
isNotEmpty(process.env.AZURE_AD_CLIENT_SECRET) &&
isNotEmpty(process.env.AZURE_AD_TENANT_ID)
) {
providers.push(
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
})
)
}
const handler = (req: NextApiRequest, res: NextApiResponse) => {
if (
req.method === 'GET' &&
req.url === '/api/auth/session' &&
env('E2E_TEST') === 'true'
) {
res.send({ user: mockedUser })
return
}
if (req.method === 'HEAD') {
res.status(200)
return
}
NextAuth(req, res, {
adapter: CustomAdapter(prisma),
secret: process.env.ENCRYPTION_SECRET,
providers,
session: {
strategy: 'database',
},
callbacks: {
session: async ({ session, user }) => {
const userFromDb = user as User
await updateLastActivityDate(userFromDb)
return {
...session,
user: userFromDb,
}
},
signIn: async ({ account, user }) => {
const userExists = user?.graphNavigation !== undefined
if (!account || (process.env.DISABLE_SIGNUP === 'true' && !userExists))
return false
const requiredGroups = getRequiredGroups(account.provider)
if (requiredGroups.length > 0) {
const userGroups = await getUserGroups(account)
return checkHasGroups(userGroups, requiredGroups)
}
return true
},
},
})
}
const updateLastActivityDate = async (user: User) => {
const datesAreOnSameDay = (first: Date, second: Date) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate()
if (!datesAreOnSameDay(user.lastActivityAt, new Date()))
await prisma.user.update({
where: { id: user.id },
data: { lastActivityAt: new Date() },
})
}
const getUserGroups = async (account: Account): Promise<string[]> => {
switch (account.provider) {
case 'gitlab': {
const getGitlabGroups = async (
accessToken: string,
page = 1
): Promise<{ full_path: string }[]> => {
const res = await fetch(
`${
process.env.GITLAB_BASE_URL || 'https://gitlab.com'
}/api/v4/groups?per_page=100&page=${page}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
)
const groups: { full_path: string }[] = await res.json()
const nextPage = parseInt(res.headers.get('X-Next-Page') || '')
if (nextPage)
groups.push(...(await getGitlabGroups(accessToken, nextPage)))
return groups
}
const groups = await getGitlabGroups(account.access_token as string)
return groups.map((group) => group.full_path)
}
default:
return []
}
}
const getRequiredGroups = (provider: string): string[] => {
switch (provider) {
case 'gitlab':
return process.env.GITLAB_REQUIRED_GROUPS?.split(',') || []
default:
return []
}
}
const checkHasGroups = (userGroups: string[], requiredGroups: string[]) =>
userGroups?.some((userGroup) => requiredGroups?.includes(userGroup))
export default withSentry(handler)

View File

@ -0,0 +1,202 @@
// Forked from https://github.com/nextauthjs/adapters/blob/main/packages/prisma/src/index.ts
import {
PrismaClient,
Prisma,
Invitation,
Plan,
WorkspaceRole,
WorkspaceInvitation,
} from 'db'
import type { Adapter, AdapterUser } from 'next-auth/adapters'
import cuid from 'cuid'
import { got } from 'got'
import { generateId } from 'utils'
type InvitationWithWorkspaceId = Invitation & {
typebot: {
workspaceId: string | null
}
}
export function CustomAdapter(p: PrismaClient): Adapter {
return {
createUser: async (data: Omit<AdapterUser, 'id'>) => {
const user = { id: cuid(), email: data.email as string }
const invitations = await p.invitation.findMany({
where: { email: user.email },
include: { typebot: { select: { workspaceId: true } } },
})
const workspaceInvitations = await p.workspaceInvitation.findMany({
where: { email: user.email },
})
if (
process.env.DISABLE_SIGNUP === 'true' &&
process.env.ADMIN_EMAIL !== data.email
)
throw Error('New users are forbidden')
const createdUser = await p.user.create({
data: {
...data,
id: user.id,
apiTokens: {
create: { name: 'Default', token: generateId(24) },
},
workspaces:
workspaceInvitations.length > 0
? undefined
: {
create: {
role: WorkspaceRole.ADMIN,
workspace: {
create: {
name: data.name
? `${data.name}'s workspace`
: `My workspace`,
...(process.env.ADMIN_EMAIL === data.email
? { plan: Plan.LIFETIME }
: {
plan: Plan.FREE,
}),
},
},
},
},
},
})
if (process.env.USER_CREATED_WEBHOOK_URL)
await got.post(process.env.USER_CREATED_WEBHOOK_URL, {
json: {
email: data.email,
name: data.name ? (data.name as string).split(' ')[0] : undefined,
},
})
if (invitations.length > 0)
await convertInvitationsToCollaborations(p, user, invitations)
if (workspaceInvitations.length > 0)
await joinWorkspaces(p, user, workspaceInvitations)
return createdUser
},
getUser: (id) => p.user.findUnique({ where: { id } }),
getUserByEmail: (email) => p.user.findUnique({ where: { email } }),
async getUserByAccount(provider_providerAccountId) {
const account = await p.account.findUnique({
where: { provider_providerAccountId },
select: { user: true },
})
return account?.user ?? null
},
updateUser: (data) => p.user.update({ where: { id: data.id }, data }),
deleteUser: (id) => p.user.delete({ where: { id } }),
linkAccount: async (data) => {
await p.account.create({
data: {
userId: data.userId,
type: data.type,
provider: data.provider,
providerAccountId: data.providerAccountId,
refresh_token: data.refresh_token,
access_token: data.access_token,
expires_at: data.expires_at,
token_type: data.token_type,
scope: data.scope,
id_token: data.id_token,
session_state: data.session_state,
oauth_token_secret: data.oauth_token_secret as string,
oauth_token: data.oauth_token as string,
refresh_token_expires_in: data.refresh_token_expires_in as number,
},
})
},
unlinkAccount: async (provider_providerAccountId) => {
await p.account.delete({ where: { provider_providerAccountId } })
},
async getSessionAndUser(sessionToken) {
const userAndSession = await p.session.findUnique({
where: { sessionToken },
include: { user: true },
})
if (!userAndSession) return null
const { user, ...session } = userAndSession
return { user, session }
},
createSession: (data) => p.session.create({ data }),
updateSession: (data) =>
p.session.update({ data, where: { sessionToken: data.sessionToken } }),
deleteSession: (sessionToken) =>
p.session.delete({ where: { sessionToken } }),
createVerificationToken: (data) => p.verificationToken.create({ data }),
async useVerificationToken(identifier_token) {
try {
return await p.verificationToken.delete({ where: { identifier_token } })
} catch (error) {
if ((error as Prisma.PrismaClientKnownRequestError).code === 'P2025')
return null
throw error
}
},
}
}
const convertInvitationsToCollaborations = async (
p: PrismaClient,
{ id, email }: { id: string; email: string },
invitations: InvitationWithWorkspaceId[]
) => {
await p.collaboratorsOnTypebots.createMany({
data: invitations.map((invitation) => ({
typebotId: invitation.typebotId,
type: invitation.type,
userId: id,
})),
})
const workspaceInvitations = invitations.reduce<InvitationWithWorkspaceId[]>(
(acc, invitation) =>
acc.some(
(inv) => inv.typebot.workspaceId === invitation.typebot.workspaceId
)
? acc
: [...acc, invitation],
[]
)
for (const invitation of workspaceInvitations) {
if (!invitation.typebot.workspaceId) continue
await p.memberInWorkspace.upsert({
where: {
userId_workspaceId: {
userId: id,
workspaceId: invitation.typebot.workspaceId,
},
},
create: {
userId: id,
workspaceId: invitation.typebot.workspaceId,
role: WorkspaceRole.GUEST,
},
update: {},
})
}
return p.invitation.deleteMany({
where: {
email,
},
})
}
const joinWorkspaces = async (
p: PrismaClient,
{ id, email }: { id: string; email: string },
invitations: WorkspaceInvitation[]
) => {
await p.memberInWorkspace.createMany({
data: invitations.map((invitation) => ({
workspaceId: invitation.workspaceId,
role: invitation.type,
userId: id,
})),
})
return p.workspaceInvitation.deleteMany({
where: {
email,
},
})
}

View File

@ -0,0 +1,51 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { Credentials } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
encrypt,
} from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res)
if (req.method === 'GET') {
const credentials = await prisma.credentials.findMany({
where: {
workspace: { id: workspaceId, members: { some: { userId: user.id } } },
},
select: { name: true, type: true, workspaceId: true, id: true },
})
return res.send({ credentials })
}
if (req.method === 'POST') {
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as Credentials
const { encryptedData, iv } = encrypt(data.data)
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId, members: { some: { userId: user.id } } },
select: { id: true },
})
if (!workspace) return forbidden(res)
const credentials = await prisma.credentials.create({
data: {
...data,
data: encryptedData,
iv,
workspaceId,
},
})
return res.send({ credentials })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,25 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res)
if (req.method === 'DELETE') {
const credentialsId = req.query.credentialsId as string | undefined
const credentials = await prisma.credentials.deleteMany({
where: {
id: credentialsId,
workspace: { id: workspaceId, members: { some: { userId: user.id } } },
},
})
return res.send({ credentials })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,60 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { Prisma } from 'db'
import prisma from '@/lib/prisma'
import { googleSheetsScopes } from './consent-url'
import { stringify } from 'querystring'
import { CredentialsType } from 'models'
import { badRequest, encrypt, notAuthenticated } from 'utils/api'
import { oauth2Client } from '@/lib/googleSheets'
import { withSentry } from '@sentry/nextjs'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const state = req.query.state as string | undefined
if (!state) return badRequest(res)
const { redirectUrl, blockId, workspaceId } = JSON.parse(
Buffer.from(state, 'base64').toString()
)
if (req.method === 'GET') {
const code = req.query.code as string | undefined
if (!workspaceId) return badRequest(res)
if (!code)
return res.status(400).send({ message: "Bad request, couldn't get code" })
const { tokens } = await oauth2Client.getToken(code)
if (!tokens?.access_token) {
console.error('Error getting oAuth tokens:')
throw new Error('ERROR')
}
oauth2Client.setCredentials(tokens)
const { email, scopes } = await oauth2Client.getTokenInfo(
tokens.access_token
)
if (!email)
return res
.status(400)
.send({ message: "Couldn't get email from getTokenInfo" })
if (googleSheetsScopes.some((scope) => !scopes.includes(scope)))
return res
.status(400)
.send({ message: "User didn't accepted required scopes" })
const { encryptedData, iv } = encrypt(tokens)
const credentials = {
name: email,
type: CredentialsType.GOOGLE_SHEETS,
workspaceId,
data: encryptedData,
iv,
} as Prisma.CredentialsUncheckedCreateInput
const { id: credentialsId } = await prisma.credentials.create({
data: credentials,
})
const queryParams = stringify({ blockId, credentialsId })
res.redirect(
`${redirectUrl}?${queryParams}` ?? `${process.env.NEXTAUTH_URL}`
)
}
}
export default withSentry(handler)

View File

@ -0,0 +1,23 @@
import { withSentry } from '@sentry/nextjs'
import { oauth2Client } from '@/lib/googleSheets'
import { NextApiRequest, NextApiResponse } from 'next'
export const googleSheetsScopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive.readonly',
]
const handler = (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: googleSheetsScopes,
prompt: 'consent',
state: Buffer.from(JSON.stringify(req.query)).toString('base64'),
})
res.status(301).redirect(url)
}
}
export default withSentry(handler)

View File

@ -0,0 +1,61 @@
import { withSentry } from '@sentry/nextjs'
import { CustomDomain } from 'db'
import { got, HTTPError } from 'got'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res)
if (req.method === 'GET') {
const customDomains = await prisma.customDomain.findMany({
where: {
workspace: { id: workspaceId, members: { some: { userId: user.id } } },
},
})
return res.send({ customDomains })
}
if (req.method === 'POST') {
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId, members: { some: { userId: user.id } } },
select: { id: true },
})
if (!workspace) return forbidden(res)
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as CustomDomain
try {
await createDomainOnVercel(data.name)
} catch (err) {
if (err instanceof HTTPError && err.response.statusCode !== 409)
return res.status(err.response.statusCode).send(err.response.body)
}
const customDomains = await prisma.customDomain.create({
data: {
...data,
workspaceId,
},
})
return res.send({ customDomains })
}
return methodNotAllowed(res)
}
const createDomainOnVercel = (name: string) =>
got.post({
url: `https://api.vercel.com/v8/projects/${process.env.VERCEL_VIEWER_PROJECT_NAME}/domains?teamId=${process.env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
json: { name },
})
export default withSentry(handler)

View File

@ -0,0 +1,33 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
import { got } from 'got'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res)
if (req.method === 'DELETE') {
const domain = req.query.domain as string
try {
await deleteDomainOnVercel(domain)
} catch {}
const customDomains = await prisma.customDomain.delete({
where: { name: domain },
})
return res.send({ customDomains })
}
return methodNotAllowed(res)
}
const deleteDomainOnVercel = (name: string) =>
got.delete({
url: `https://api.vercel.com/v8/projects/${process.env.VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${process.env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
})
export default withSentry(handler)

View File

@ -0,0 +1,69 @@
import { withSentry } from '@sentry/nextjs'
import { DashboardFolder, WorkspaceRole } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const parentFolderId = req.query.parentId
? req.query.parentId.toString()
: null
if (req.method === 'GET') {
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res)
const folders = await prisma.dashboardFolder.findMany({
where: {
AND: [
{
parentFolderId,
workspaceId: workspaceId,
},
{
OR: [
{
workspace: {
members: {
some: {
userId: user.id,
role: { not: WorkspaceRole.GUEST },
},
},
},
},
{
typebots: {
some: {
collaborators: {
some: {
userId: user.id,
},
},
},
},
},
],
},
],
},
orderBy: { createdAt: 'desc' },
})
return res.send({ folders })
}
if (req.method === 'POST') {
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as Pick<DashboardFolder, 'parentFolderId' | 'workspaceId'>
const folder = await prisma.dashboardFolder.create({
data: { ...data, name: 'New folder' },
})
return res.send(folder)
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,44 @@
import { withSentry } from '@sentry/nextjs'
import { DashboardFolder } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const id = req.query.id as string
if (req.method === 'GET') {
const folder = await prisma.dashboardFolder.findFirst({
where: {
id,
workspace: { members: { some: { userId: user.id } } },
},
})
return res.send({ folder })
}
if (req.method === 'DELETE') {
const folders = await prisma.dashboardFolder.deleteMany({
where: { id, workspace: { members: { some: { userId: user.id } } } },
})
return res.send({ folders })
}
if (req.method === 'PATCH') {
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as Partial<DashboardFolder>
const folders = await prisma.dashboardFolder.updateMany({
where: {
id,
workspace: { members: { some: { userId: user.id } } },
},
data,
})
return res.send({ typebots: folders })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,40 @@
import { captureException, withSentry } from '@sentry/nextjs'
import { SmtpCredentialsData } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { createTransport } from 'nodemailer'
import { getAuthenticatedUser } from '@/features/auth'
import { notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'POST') {
const { from, port, isTlsEnabled, username, password, host, to } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as SmtpCredentialsData & { to: string }
const transporter = createTransport({
host,
port,
secure: isTlsEnabled ?? undefined,
auth: {
user: username,
pass: password,
},
})
try {
const info = await transporter.sendMail({
from: `"${from.name}" <${from.email}>`,
to,
subject: 'Your SMTP configuration is working 🤩',
text: 'This email has been sent to test out your SMTP config.\n\nIf your read this then it has been successful.🚀',
})
res.status(200).send({ message: 'Email sent!', info })
} catch (err) {
captureException(err)
console.log(err)
res.status(500).send(err)
}
}
}
export default withSentry(handler)

View File

@ -0,0 +1,33 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { drive } from '@googleapis/drive'
import { getAuthenticatedGoogleClient } from '@/lib/googleSheets'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
import { setUser, withSentry } from '@sentry/nextjs'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
setUser({ email: user.email ?? undefined, id: user.id })
if (req.method === 'GET') {
const credentialsId = req.query.credentialsId as string | undefined
if (!credentialsId) return badRequest(res)
const auth = await getAuthenticatedGoogleClient(user.id, credentialsId)
if (!auth)
return res.status(404).send("Couldn't find credentials in database")
const response = await drive({
version: 'v3',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
auth: auth.client,
}).files.list({
q: "mimeType='application/vnd.google-apps.spreadsheet'",
fields: 'nextPageToken, files(id, name)',
})
return res.send(response.data)
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,49 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { GoogleSpreadsheet } from 'google-spreadsheet'
import { getAuthenticatedGoogleClient } from '@/lib/googleSheets'
import { isDefined } from 'utils'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
import { withSentry, setUser } from '@sentry/nextjs'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
setUser({ email: user.email ?? undefined, id: user.id })
if (req.method === 'GET') {
const credentialsId = req.query.credentialsId as string | undefined
if (!credentialsId) return badRequest(res)
const spreadsheetId = req.query.id as string
const doc = new GoogleSpreadsheet(spreadsheetId)
const auth = await getAuthenticatedGoogleClient(user.id, credentialsId)
if (!auth)
return res
.status(404)
.send({ message: "Couldn't find credentials in database" })
doc.useOAuth2Client(auth.client)
await doc.loadInfo()
return res.send({
sheets: (
await Promise.all(
Array.from(Array(doc.sheetCount)).map(async (_, idx) => {
const sheet = doc.sheetsByIndex[idx]
try {
await sheet.loadHeaderRow()
} catch (err) {
return
}
return {
id: sheet.sheetId,
name: sheet.title,
columns: sheet.headerValues,
}
})
)
).filter(isDefined),
})
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,12 @@
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).send(req.body)
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,27 @@
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') {
const firstParam = req.query.firstParam?.toString()
const secondParam = req.query.secondParam?.toString()
const customHeader = req.headers['custom-typebot']
const { body } = req
if (
body.customField === 'secret4' &&
customHeader === 'secret3' &&
secondParam === 'secret2' &&
firstParam === 'secret1'
) {
return res.status(200).send([
{ name: 'John', age: 21 },
{ name: 'Tim', age: 52 },
])
}
return res.status(400).send({ message: 'Bad request' })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,19 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const publicId = req.query.publicId as string | undefined
if (!publicId) return badRequest(res, 'publicId is required')
const exists = await prisma.typebot.count({ where: { publicId } })
return res.send({ isAvailable: Boolean(!exists) })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,42 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { InputBlockType, PublicTypebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { canPublishFileInput } from '@/utils/api/dbRules'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
try {
if (req.method === 'POST') {
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res, 'workspaceId is required')
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as PublicTypebot
const typebotContainsFileInput = data.groups
.flatMap((g) => g.blocks)
.some((b) => b.type === InputBlockType.FILE)
if (
typebotContainsFileInput &&
!(await canPublishFileInput({ userId: user.id, workspaceId, res }))
)
return
const typebot = await prisma.publicTypebot.create({
data: { ...data },
})
return res.send(typebot)
}
return methodNotAllowed(res)
} catch (err) {
console.error(err)
if (err instanceof Error) {
return res.status(500).send({ title: err.name, message: err.message })
}
return res.status(500).send({ message: 'An error occured', error: err })
}
}
export default withSentry(handler)

View File

@ -0,0 +1,50 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { InputBlockType, PublicTypebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { canPublishFileInput, canWriteTypebot } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const id = req.query.id as string
const workspaceId = req.query.workspaceId as string | undefined
if (req.method === 'PUT') {
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as PublicTypebot
if (!workspaceId) return badRequest(res, 'workspaceId is required')
const typebotContainsFileInput = data.groups
.flatMap((g) => g.blocks)
.some((b) => b.type === InputBlockType.FILE)
if (
typebotContainsFileInput &&
!(await canPublishFileInput({ userId: user.id, workspaceId, res }))
)
return
const typebots = await prisma.publicTypebot.update({
where: { id },
data,
})
return res.send({ typebots })
}
if (req.method === 'DELETE') {
const publishedTypebotId = req.query.id as string
const typebotId = req.query.typebotId as string | undefined
if (!typebotId) return badRequest(res, 'typebotId is required')
await prisma.publicTypebot.deleteMany({
where: {
id: publishedTypebotId,
typebot: canWriteTypebot(typebotId, user),
},
})
return res.send({ success: true })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,39 @@
import { withSentry } from '@sentry/nextjs'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import {
badRequest,
generatePresignedUrl,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
const handler = async (
req: NextApiRequest,
res: NextApiResponse
): Promise<void> => {
res.setHeader('Access-Control-Allow-Origin', '*')
if (req.method === 'GET') {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
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
if (!filePath || !fileType) return badRequest(res)
const presignedUrl = generatePresignedUrl({ fileType, filePath })
return res.status(200).send({ presignedUrl })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,42 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
import Stripe from 'stripe'
import { withSentry } from '@sentry/nextjs'
import { getAuthenticatedUser } from '@/features/auth'
import prisma from '@/lib/prisma'
import { WorkspaceRole } from 'db'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const session = await stripe.billingPortal.sessions.create({
customer: workspace.stripeId,
return_url: req.headers.referer,
})
res.redirect(session.url)
return
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,61 @@
import { withSentry } from '@sentry/nextjs'
import { Plan } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import Stripe from 'stripe'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const session = await createCheckoutSession(user.id)
if (!session?.url) return res.redirect('/typebots')
return res.redirect(session.url)
}
return methodNotAllowed(res)
}
const createCheckoutSession = async (userId: string) => {
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const claimableCustomPlan = await prisma.claimableCustomPlan.findFirst({
where: { workspace: { members: { some: { userId } } } },
})
if (!claimableCustomPlan) return null
return stripe.checkout.sessions.create({
success_url: `${process.env.NEXTAUTH_URL}/typebots?stripe=${Plan.CUSTOM}&success=true`,
cancel_url: `${process.env.NEXTAUTH_URL}/typebots?stripe=cancel`,
mode: 'subscription',
metadata: {
claimableCustomPlanId: claimableCustomPlan.id,
},
currency: claimableCustomPlan.currency,
automatic_tax: { enabled: true },
line_items: [
{
price_data: {
currency: claimableCustomPlan.currency,
tax_behavior: 'exclusive',
recurring: { interval: 'month' },
product_data: {
name: claimableCustomPlan.name,
description: claimableCustomPlan.description ?? undefined,
},
unit_amount: claimableCustomPlan.price * 100,
},
quantity: 1,
},
],
})
}
export default withSentry(handler)

View File

@ -0,0 +1,49 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
import Stripe from 'stripe'
import { withSentry } from '@sentry/nextjs'
import { getAuthenticatedUser } from '@/features/auth'
import prisma from '@/lib/prisma'
import { WorkspaceRole } from 'db'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const invoices = await stripe.invoices.list({
customer: workspace.stripeId,
})
res.send({
invoices: invoices.data.map((i) => ({
id: i.number,
url: i.invoice_pdf,
amount: i.subtotal,
currency: i.currency,
date: i.status_transitions.paid_at,
})),
})
return
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,260 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { isDefined } from 'utils'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
import Stripe from 'stripe'
import { withSentry } from '@sentry/nextjs'
import { getAuthenticatedUser } from '@/features/auth'
import prisma from '@/lib/prisma'
import { Plan, WorkspaceRole } from 'db'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET')
return res.send(await getSubscriptionDetails(req, res)(user.id))
if (req.method === 'POST') {
const session = await createCheckoutSession(req)
return res.send({ sessionId: session.id })
}
if (req.method === 'PUT') {
await updateSubscription(req)
return res.send({ message: 'success' })
}
if (req.method === 'DELETE') {
await cancelSubscription(req, res)(user.id)
return res.send({ message: 'success' })
}
return methodNotAllowed(res)
}
const getSubscriptionDetails =
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const subscriptions = await stripe.subscriptions.list({
customer: workspace.stripeId,
limit: 1,
})
return {
additionalChatsIndex:
subscriptions.data[0]?.items.data.find(
(item) =>
item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
)?.quantity ?? 0,
additionalStorageIndex:
subscriptions.data[0]?.items.data.find(
(item) =>
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)?.quantity ?? 0,
}
}
const createCheckoutSession = (req: NextApiRequest) => {
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const {
email,
currency,
plan,
workspaceId,
href,
additionalChats,
additionalStorage,
} = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
return stripe.checkout.sessions.create({
success_url: `${href}?stripe=${plan}&success=true`,
cancel_url: `${href}?stripe=cancel`,
allow_promotion_codes: true,
customer_email: email,
mode: 'subscription',
metadata: { workspaceId, plan, additionalChats, additionalStorage },
currency,
automatic_tax: { enabled: true },
line_items: parseSubscriptionItems(
plan,
additionalChats,
additionalStorage
),
})
}
const updateSubscription = async (req: NextApiRequest) => {
const {
stripeId,
plan,
workspaceId,
additionalChats,
additionalStorage,
currency,
} = (typeof req.body === 'string' ? JSON.parse(req.body) : req.body) as {
stripeId: string
workspaceId: string
additionalChats: number
additionalStorage: number
plan: 'STARTER' | 'PRO'
currency: 'eur' | 'usd'
}
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const { data } = await stripe.subscriptions.list({
customer: stripeId,
})
const subscription = data[0] as Stripe.Subscription | undefined
const currentStarterPlanItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_STARTER_PRICE_ID
)?.id
const currentProPlanItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_PRO_PRICE_ID
)?.id
const currentAdditionalChatsItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
)?.id
const currentAdditionalStorageItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)?.id
const items = [
{
id: currentStarterPlanItemId ?? currentProPlanItemId,
price:
plan === Plan.STARTER
? process.env.STRIPE_STARTER_PRICE_ID
: process.env.STRIPE_PRO_PRICE_ID,
quantity: 1,
},
additionalChats === 0 && !currentAdditionalChatsItemId
? undefined
: {
id: currentAdditionalChatsItemId,
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
quantity: additionalChats,
deleted: subscription ? additionalChats === 0 : undefined,
},
additionalStorage === 0 && !currentAdditionalStorageItemId
? undefined
: {
id: currentAdditionalStorageItemId,
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
quantity: additionalStorage,
deleted: subscription ? additionalStorage === 0 : undefined,
},
].filter(isDefined)
if (subscription) {
await stripe.subscriptions.update(subscription.id, {
items,
})
} else {
await stripe.subscriptions.create({
customer: stripeId,
items,
currency,
})
}
await prisma.workspace.update({
where: { id: workspaceId },
data: {
plan,
additionalChatsIndex: additionalChats,
additionalStorageIndex: additionalStorage,
chatsLimitFirstEmailSentAt: null,
chatsLimitSecondEmailSentAt: null,
storageLimitFirstEmailSentAt: null,
storageLimitSecondEmailSentAt: null,
},
})
}
const cancelSubscription =
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const existingSubscription = await stripe.subscriptions.list({
customer: workspace.stripeId,
})
const currentSubscriptionId = existingSubscription.data[0]?.id
if (currentSubscriptionId)
await stripe.subscriptions.del(currentSubscriptionId)
await prisma.workspace.update({
where: { id: workspace.id },
data: {
plan: Plan.FREE,
additionalChatsIndex: 0,
additionalStorageIndex: 0,
},
})
}
const parseSubscriptionItems = (
plan: Plan,
additionalChats: number,
additionalStorage: number
) =>
[
{
price:
plan === Plan.STARTER
? process.env.STRIPE_STARTER_PRICE_ID
: process.env.STRIPE_PRO_PRICE_ID,
quantity: 1,
},
]
.concat(
additionalChats > 0
? [
{
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
quantity: additionalChats,
},
]
: []
)
.concat(
additionalStorage > 0
? [
{
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
quantity: additionalStorage,
},
]
: []
)
export default withSentry(handler)

View File

@ -0,0 +1,132 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'utils/api'
import Stripe from 'stripe'
import Cors from 'micro-cors'
import { buffer } from 'micro'
import prisma from '@/lib/prisma'
import { withSentry } from '@sentry/nextjs'
import { Plan } from 'db'
if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET)
throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01',
})
const cors = Cors({
allowMethods: ['POST', 'HEAD'],
})
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string
export const config = {
api: {
bodyParser: false,
},
}
const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const buf = await buffer(req)
const sig = req.headers['stripe-signature']
if (!sig) return res.status(400).send(`stripe-signature is missing`)
try {
const event = stripe.webhooks.constructEvent(
buf.toString(),
sig.toString(),
webhookSecret
)
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
const metadata = session.metadata as unknown as
| {
plan: 'STARTER' | 'PRO'
additionalChats: string
additionalStorage: string
workspaceId: string
}
| { claimableCustomPlanId: string }
if ('plan' in metadata) {
const { workspaceId, plan, additionalChats, additionalStorage } =
metadata
if (!workspaceId || !plan || !additionalChats || !additionalStorage)
return res
.status(500)
.send({ message: `Couldn't retrieve valid metadata` })
await prisma.workspace.update({
where: { id: workspaceId },
data: {
plan: plan,
stripeId: session.customer as string,
additionalChatsIndex: parseInt(additionalChats),
additionalStorageIndex: parseInt(additionalStorage),
chatsLimitFirstEmailSentAt: null,
chatsLimitSecondEmailSentAt: null,
storageLimitFirstEmailSentAt: null,
storageLimitSecondEmailSentAt: null,
},
})
} else {
const { claimableCustomPlanId } = metadata
if (!claimableCustomPlanId)
return res
.status(500)
.send({ message: `Couldn't retrieve valid metadata` })
const { workspaceId, chatsLimit, seatsLimit, storageLimit } =
await prisma.claimableCustomPlan.update({
where: { id: claimableCustomPlanId },
data: { claimedAt: new Date() },
})
await prisma.workspace.update({
where: { id: workspaceId },
data: {
plan: Plan.CUSTOM,
stripeId: session.customer as string,
customChatsLimit: chatsLimit,
customStorageLimit: storageLimit,
customSeatsLimit: seatsLimit,
},
})
}
return res.status(200).send({ message: 'workspace upgraded in DB' })
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
await prisma.workspace.update({
where: {
stripeId: subscription.customer as string,
},
data: {
plan: Plan.FREE,
additionalChatsIndex: 0,
additionalStorageIndex: 0,
chatsLimitFirstEmailSentAt: null,
chatsLimitSecondEmailSentAt: null,
storageLimitFirstEmailSentAt: null,
storageLimitSecondEmailSentAt: null,
},
})
return res.send({ message: 'workspace downgraded in DB' })
}
default: {
return res.status(304).send({ message: 'event not handled' })
}
}
} catch (err) {
console.error(err)
if (err instanceof Error) {
console.error(err)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
return res.status(500).send(`Error occured: ${err}`)
}
}
return methodNotAllowed(res)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default withSentry(cors(webhookHandler as any))

View File

@ -0,0 +1,115 @@
import { withSentry } from '@sentry/nextjs'
import { Plan, Prisma, WorkspaceRole } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import {
badRequest,
methodNotAllowed,
notAuthenticated,
notFound,
} from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
import { parseNewTypebot } from '@/features/dashboard'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
try {
if (req.method === 'GET') {
const workspaceId = req.query.workspaceId as string | undefined
const folderId = req.query.allFolders
? undefined
: req.query.folderId
? req.query.folderId.toString()
: null
if (!workspaceId) return badRequest(res)
const typebotIds = req.query.typebotIds as string[] | undefined
if (typebotIds) {
const typebots = await prisma.typebot.findMany({
where: {
OR: [
{
workspace: { members: { some: { userId: user.id } } },
id: { in: typebotIds },
isArchived: { not: true },
},
{
id: { in: typebotIds },
collaborators: {
some: {
userId: user.id,
},
},
isArchived: { not: true },
},
],
},
orderBy: { createdAt: 'desc' },
select: { name: true, id: true, groups: true, variables: true },
})
return res.send({ typebots })
}
const typebots = await prisma.typebot.findMany({
where: {
OR: [
{
isArchived: { not: true },
folderId,
workspace: {
id: workspaceId,
members: {
some: {
userId: user.id,
role: { not: WorkspaceRole.GUEST },
},
},
},
},
{
isArchived: { not: true },
workspace: {
id: workspaceId,
members: {
some: { userId: user.id, role: WorkspaceRole.GUEST },
},
},
collaborators: { some: { userId: user.id } },
},
],
},
orderBy: { createdAt: 'desc' },
select: { name: true, publishedTypebotId: true, id: true, icon: true },
})
return res.send({ typebots })
}
if (req.method === 'POST') {
const workspace = await prisma.workspace.findFirst({
where: { id: req.body.workspaceId },
select: { plan: true },
})
if (!workspace) return notFound(res, "Couldn't find workspace")
const data =
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const typebot = await prisma.typebot.create({
data:
'groups' in data
? data
: (parseNewTypebot({
ownerAvatarUrl: user.image,
isBrandingEnabled: workspace.plan === Plan.FREE,
...data,
}) as Prisma.TypebotUncheckedCreateInput),
})
return res.send(typebot)
}
return methodNotAllowed(res)
} catch (err) {
console.error(err)
if (err instanceof Error) {
return res.status(500).send({ title: err.name, message: err.message })
}
return res.status(500).send({ message: 'An error occured', error: err })
}
}
export default withSentry(handler)

View File

@ -0,0 +1,88 @@
import { withSentry } from '@sentry/nextjs'
import { CollaborationType } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot, canWriteTypebot } from '@/utils/api/dbRules'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
import { archiveResults } from '@/features/results/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const typebotId = req.query.typebotId as string
if (req.method === 'GET') {
const typebot = await prisma.typebot.findFirst({
where: {
...canReadTypebot(typebotId, user),
isArchived: { not: true },
},
include: {
publishedTypebot: true,
collaborators: { select: { userId: true, type: true } },
webhooks: true,
},
})
if (!typebot) return res.send({ typebot: null })
const { publishedTypebot, collaborators, webhooks, ...restOfTypebot } =
typebot
const isReadOnly =
collaborators.find((c) => c.userId === user.id)?.type ===
CollaborationType.READ
return res.send({
typebot: restOfTypebot,
publishedTypebot,
isReadOnly,
webhooks,
})
}
if (req.method === 'DELETE') {
await archiveResults(res)({
typebotId,
user,
resultsFilter: { typebotId },
})
await prisma.publicTypebot.deleteMany({
where: { typebot: canWriteTypebot(typebotId, user) },
})
const typebots = await prisma.typebot.updateMany({
where: canWriteTypebot(typebotId, user),
data: { isArchived: true },
})
return res.send({ typebots })
}
if (req.method === 'PUT') {
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const existingTypebot = await prisma.typebot.findFirst({
where: canReadTypebot(typebotId, user),
})
if (
existingTypebot &&
existingTypebot?.updatedAt > new Date(data.updatedAt)
)
return res.send({ message: 'Found newer version of typebot in database' })
const typebots = await prisma.typebot.updateMany({
where: canWriteTypebot(typebotId, user),
data: {
...data,
theme: data.theme ?? undefined,
settings: data.settings ?? undefined,
resultsTablePreferences: data.resultsTablePreferences ?? undefined,
},
})
return res.send({ typebots })
}
if (req.method === 'PATCH') {
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const typebots = await prisma.typebot.updateMany({
where: canWriteTypebot(typebotId, user),
data,
})
return res.send({ typebots })
}
return 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 { canReadTypebot } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated, notFound } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const typebot = await prisma.typebot.findFirst({
where: canReadTypebot(typebotId, user),
})
if (!typebot) return notFound(res)
return res.send({ groups: typebot.groups })
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,24 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const typebotId = req.query.typebotId as string
if (req.method === 'GET') {
const collaborators = await prisma.collaboratorsOnTypebots.findMany({
where: { typebot: canReadTypebot(typebotId, user) },
include: { user: { select: { name: true, image: true, email: true } } },
})
return res.send({
collaborators,
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,34 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canEditGuests } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const typebotId = req.query.typebotId as string
const userId = req.query.userId as string
if (req.method === 'PATCH') {
const data = req.body
await prisma.collaboratorsOnTypebots.updateMany({
where: { userId, typebot: canEditGuests(user, typebotId) },
data: { type: data.type },
})
return res.send({
message: 'success',
})
}
if (req.method === 'DELETE') {
await prisma.collaboratorsOnTypebots.deleteMany({
where: { userId, typebot: canEditGuests(user, typebotId) },
})
return res.send({
message: 'success',
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,85 @@
import { withSentry } from '@sentry/nextjs'
import { CollaborationType, WorkspaceRole } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot, canWriteTypebot } from '@/utils/api/dbRules'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
import { env } from 'utils'
import { sendGuestInvitationEmail } from 'emails'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const typebotId = req.query.typebotId as string
if (req.method === 'GET') {
const invitations = await prisma.invitation.findMany({
where: { typebotId, typebot: canReadTypebot(typebotId, user) },
})
return res.send({
invitations,
})
}
if (req.method === 'POST') {
const typebot = await prisma.typebot.findFirst({
where: canWriteTypebot(typebotId, user),
include: { workspace: { select: { name: true } } },
})
if (!typebot || !typebot.workspaceId) return forbidden(res)
const { email, type } =
(req.body as
| { email: string | undefined; type: CollaborationType | undefined }
| undefined) ?? {}
if (!email || !type) return badRequest(res)
const existingUser = await prisma.user.findUnique({
where: { email: email.toLowerCase() },
select: { id: true },
})
if (existingUser) {
await prisma.collaboratorsOnTypebots.create({
data: {
type,
typebotId,
userId: existingUser.id,
},
})
await prisma.memberInWorkspace.upsert({
where: {
userId_workspaceId: {
userId: existingUser.id,
workspaceId: typebot.workspaceId,
},
},
create: {
role: WorkspaceRole.GUEST,
userId: existingUser.id,
workspaceId: typebot.workspaceId,
},
update: {},
})
} else
await prisma.invitation.create({
data: { email: email.toLowerCase().trim(), type, typebotId },
})
if (env('E2E_TEST') !== 'true')
await sendGuestInvitationEmail({
to: email,
hostEmail: user.email ?? '',
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${typebot.workspaceId}`,
guestEmail: email.toLowerCase(),
typebotName: typebot.name,
workspaceName: typebot.workspace?.name ?? '',
})
return res.send({
message: 'success',
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,38 @@
import { withSentry } from '@sentry/nextjs'
import { Invitation } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canEditGuests } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const typebotId = req.query.typebotId as string
const email = req.query.email as string
if (req.method === 'PATCH') {
const data = req.body as Invitation
await prisma.invitation.updateMany({
where: { email, typebot: canEditGuests(user, typebotId) },
data: { type: data.type },
})
return res.send({
message: 'success',
})
}
if (req.method === 'DELETE') {
await prisma.invitation.deleteMany({
where: {
email,
typebot: canEditGuests(user, typebotId),
},
})
return res.send({
message: 'success',
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,67 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot, canWriteTypebot } from '@/utils/api/dbRules'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
import { archiveResults } from '@/features/results/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res, 'workspaceId is required')
const workspace = await prisma.workspace.findFirst({
where:
user.email === process.env.ADMIN_EMAIL
? undefined
: { id: workspaceId, members: { some: { userId: user.id } } },
select: { plan: true },
})
if (!workspace) return forbidden(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const lastResultId = req.query.lastResultId?.toString()
const take = Number(req.query.limit?.toString())
const results = await prisma.result.findMany({
take: isNaN(take) ? undefined : take,
skip: lastResultId ? 1 : 0,
cursor: lastResultId
? {
id: lastResultId,
}
: undefined,
where: {
typebot: canReadTypebot(typebotId, user),
answers: { some: {} },
},
orderBy: {
createdAt: 'desc',
},
include: { answers: true },
})
return res.status(200).send({ results })
}
if (req.method === 'DELETE') {
const typebotId = req.query.typebotId as string
const data = req.body as { ids: string[] }
const ids = data.ids
await archiveResults(res)({
typebotId,
user,
resultsFilter: {
id: ids.length > 0 ? { in: ids } : undefined,
typebot: canWriteTypebot(typebotId, user),
},
})
return res.status(200).send({ message: 'done' })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,24 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const resultId = req.query.resultId as string
const logs = await prisma.log.findMany({
where: {
result: { id: resultId, typebot: canReadTypebot(typebotId, user) },
},
})
return res.send({ logs })
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,38 @@
import { PublicTypebot } from 'models'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
import { withSentry } from '@sentry/nextjs'
import { getAuthenticatedUser } from '@/features/auth'
import { canReadTypebot } from '@/utils/api/dbRules'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const typebot = await prisma.typebot.findFirst({
where: canReadTypebot(typebotId, user),
include: { publishedTypebot: true },
})
const publishedTypebot =
typebot?.publishedTypebot as unknown as PublicTypebot
if (!publishedTypebot) return res.status(404).send({ answersCounts: [] })
const answersCounts = await prisma.answer.groupBy({
by: ['groupId'],
where: {
groupId: { in: publishedTypebot.groups.map((g) => g.id) },
},
_count: { _all: true },
})
return res.status(200).send({
answersCounts: answersCounts.map((answer) => ({
groupId: answer.groupId,
totalAnswers: answer._count._all,
})),
})
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,45 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { Stats } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const totalViews = await prisma.result.count({
where: {
typebotId,
typebot: canReadTypebot(typebotId, user),
},
})
const totalStarts = await prisma.result.count({
where: {
typebotId,
typebot: canReadTypebot(typebotId, user),
answers: { some: {} },
},
})
const totalCompleted = await prisma.result.count({
where: {
typebotId,
typebot: canReadTypebot(typebotId, user),
isCompleted: true,
},
})
const stats: Stats = {
totalViews,
totalStarts,
totalCompleted,
}
return res.status(200).send({ stats })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,26 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { defaultWebhookAttributes } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { canWriteTypebot } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { forbidden, methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'POST') {
const typebotId = req.query.typebotId as string
const typebot = await prisma.typebot.findFirst({
where: canWriteTypebot(typebotId, user),
})
if (!typebot) return forbidden(res)
const webhook = await prisma.webhook.create({
data: { typebotId, ...defaultWebhookAttributes },
})
return res.send({ webhook })
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,23 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const id = req.query.userId as string
if (req.method === 'PUT') {
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const typebots = await prisma.user.update({
where: { id },
data,
})
return res.send({ typebots })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,40 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { generateId } from 'utils'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const apiTokens = await prisma.apiToken.findMany({
where: { ownerId: user.id },
select: {
id: true,
name: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
})
return res.send({ apiTokens })
}
if (req.method === 'POST') {
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const apiToken = await prisma.apiToken.create({
data: { name: data.name, ownerId: user.id, token: generateId(24) },
})
return res.send({
apiToken: {
id: apiToken.id,
name: apiToken.name,
createdAt: apiToken.createdAt,
token: apiToken.token,
},
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,21 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'DELETE') {
const id = req.query.tokenId as string
const apiToken = await prisma.apiToken.delete({
where: { id },
})
return res.send({ apiToken })
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,71 @@
import { withSentry } from '@sentry/nextjs'
import { CollaborationType } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const webhookId = req.query.webhookId as string
if (req.method === 'GET') {
const webhook = await prisma.webhook.findFirst({
where: {
id: webhookId,
typebot: {
OR: [
{ workspace: { members: { some: { userId: user.id } } } },
{
collaborators: {
some: {
userId: user.id,
},
},
},
],
},
},
})
return res.send({ webhook })
}
if (req.method === 'PUT') {
const data = req.body
if (!('typebotId' in data)) return badRequest(res)
const typebot = await prisma.typebot.findFirst({
where: {
OR: [
{
id: data.typebotId,
workspace: { members: { some: { userId: user.id } } },
},
{
collaborators: {
some: {
userId: user.id,
type: CollaborationType.WRITE,
},
},
},
],
},
})
if (!typebot) return forbidden(res)
const webhook = await prisma.webhook.upsert({
where: {
id: webhookId,
},
create: data,
update: data,
})
return res.send({ webhook })
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,36 @@
import { withSentry } from '@sentry/nextjs'
import { Plan, Workspace } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const workspaces = await prisma.workspace.findMany({
where: { members: { some: { userId: user.id } } },
include: { members: true },
orderBy: { createdAt: 'asc' },
})
return res.send({ workspaces })
}
if (req.method === 'POST') {
const data = req.body as Workspace
const workspace = await prisma.workspace.create({
data: {
...data,
members: { create: [{ role: 'ADMIN', userId: user.id }] },
plan:
process.env.ADMIN_EMAIL === user.email ? Plan.LIFETIME : Plan.FREE,
},
})
return res.status(200).json({
workspace,
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,41 @@
import { withSentry } from '@sentry/nextjs'
import { Prisma, Workspace, WorkspaceRole } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'PATCH') {
const id = req.query.workspaceId as string
const updates = req.body as Partial<Workspace>
const updatedWorkspace = await prisma.workspace.updateMany({
where: {
id,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
data: updates,
})
return res.status(200).json({
workspace: updatedWorkspace,
})
}
if (req.method === 'DELETE') {
const id = req.query.workspaceId as string
const workspaceFilter: Prisma.WorkspaceWhereInput = {
id,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
}
await prisma.workspace.deleteMany({
where: workspaceFilter,
})
return res.status(200).json({
message: 'success',
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,77 @@
import { withSentry } from '@sentry/nextjs'
import { Workspace, WorkspaceInvitation, WorkspaceRole } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { forbidden, methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
import { env, getSeatsLimit } from 'utils'
import { sendWorkspaceMemberInvitationEmail } from 'emails'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'POST') {
const data = req.body as Omit<WorkspaceInvitation, 'id' | 'createdAt'>
const existingUser = await prisma.user.findUnique({
where: { email: data.email },
})
const workspace = await prisma.workspace.findFirst({
where: {
id: data.workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace) return forbidden(res)
if (await checkIfSeatsLimitReached(workspace))
return res.status(400).send('Seats limit reached')
if (existingUser) {
await prisma.memberInWorkspace.create({
data: {
role: data.type,
workspaceId: data.workspaceId,
userId: existingUser.id,
},
})
if (env('E2E_TEST') !== 'true')
await sendWorkspaceMemberInvitationEmail({
to: data.email,
workspaceName: workspace.name,
guestEmail: data.email,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
hostEmail: user.email ?? '',
})
return res.send({
member: {
userId: existingUser.id,
name: existingUser.name,
email: existingUser.email,
role: data.type,
workspaceId: data.workspaceId,
},
})
} else {
const invitation = await prisma.workspaceInvitation.create({ data })
if (env('E2E_TEST') !== 'true')
await sendWorkspaceMemberInvitationEmail({
to: data.email,
workspaceName: workspace.name,
guestEmail: data.email,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
hostEmail: user.email ?? '',
})
return res.send({ invitation })
}
}
methodNotAllowed(res)
}
const checkIfSeatsLimitReached = async (workspace: Workspace) => {
const existingMembersCount = await prisma.memberInWorkspace.count({
where: { workspaceId: workspace.id },
})
return existingMembersCount >= getSeatsLimit(workspace)
}
export default withSentry(handler)

View File

@ -0,0 +1,39 @@
import { withSentry } from '@sentry/nextjs'
import { WorkspaceInvitation, WorkspaceRole } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'PATCH') {
const data = req.body as Omit<WorkspaceInvitation, 'createdAt'>
const invitation = await prisma.workspaceInvitation.updateMany({
where: {
id: data.id,
workspace: {
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
},
data,
})
return res.send({ invitation })
}
if (req.method === 'DELETE') {
const id = req.query.id as string
await prisma.workspaceInvitation.deleteMany({
where: {
id,
workspace: {
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
},
})
return res.send({ message: 'success' })
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,42 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated, notFound } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const id = req.query.workspaceId as string
const workspace = await prisma.workspace.findFirst({
where: {
id,
members: { some: { userId: user.id } },
},
include: {
members: {
include: {
user: true,
},
},
invitations: true,
},
})
if (!workspace) return notFound(res)
return res.send({
members: workspace.members.map((member) => ({
userId: member.userId,
role: member.role,
workspaceId: member.workspaceId,
email: member.user.email,
image: member.user.image,
name: member.user.name,
})),
invitations: workspace.invitations,
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,48 @@
import { withSentry } from '@sentry/nextjs'
import { MemberInWorkspace, WorkspaceRole } from 'db'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'PATCH') {
const workspaceId = req.query.workspaceId as string
const memberId = req.query.id as string
const updates = req.body as Partial<MemberInWorkspace>
const member = await prisma.memberInWorkspace.updateMany({
where: {
userId: memberId,
workspace: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
},
data: { role: updates.role },
})
return res.status(200).json({
member,
})
}
if (req.method === 'DELETE') {
const workspaceId = req.query.workspaceId as string
const memberId = req.query.id as string
const member = await prisma.memberInWorkspace.deleteMany({
where: {
userId: memberId,
workspace: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
},
})
return res.status(200).json({
member,
})
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,54 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const workspaceId = req.query.workspaceId as string
const now = new Date()
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
const totalChatsUsed = await prisma.result.count({
where: {
typebot: {
workspace: {
id: workspaceId,
members: { some: { userId: user.id } },
},
},
hasStarted: true,
createdAt: {
gte: firstDayOfMonth,
lte: lastDayOfMonth,
},
},
})
const {
_sum: { storageUsed: totalStorageUsed },
} = await prisma.answer.aggregate({
where: {
storageUsed: { gt: 0 },
result: {
typebot: {
workspace: {
id: workspaceId,
members: { some: { userId: user.id } },
},
},
},
},
_sum: { storageUsed: true },
})
return res.send({
totalChatsUsed,
totalStorageUsed,
})
}
methodNotAllowed(res)
}
export default withSentry(handler)