♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
197
apps/builder/src/pages/api/auth/[...nextauth].ts
Normal file
197
apps/builder/src/pages/api/auth/[...nextauth].ts
Normal 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)
|
202
apps/builder/src/pages/api/auth/adapter.ts
Normal file
202
apps/builder/src/pages/api/auth/adapter.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
51
apps/builder/src/pages/api/credentials.ts
Normal file
51
apps/builder/src/pages/api/credentials.ts
Normal 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)
|
25
apps/builder/src/pages/api/credentials/[credentialsId].ts
Normal file
25
apps/builder/src/pages/api/credentials/[credentialsId].ts
Normal 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)
|
@ -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)
|
@ -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)
|
61
apps/builder/src/pages/api/customDomains.ts
Normal file
61
apps/builder/src/pages/api/customDomains.ts
Normal 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)
|
33
apps/builder/src/pages/api/customDomains/[domain].ts
Normal file
33
apps/builder/src/pages/api/customDomains/[domain].ts
Normal 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)
|
69
apps/builder/src/pages/api/folders.ts
Normal file
69
apps/builder/src/pages/api/folders.ts
Normal 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)
|
44
apps/builder/src/pages/api/folders/[id].ts
Normal file
44
apps/builder/src/pages/api/folders/[id].ts
Normal 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)
|
40
apps/builder/src/pages/api/integrations/email/test-config.ts
Normal file
40
apps/builder/src/pages/api/integrations/email/test-config.ts
Normal 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)
|
@ -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)
|
@ -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)
|
12
apps/builder/src/pages/api/mock/webhook-easy-config.ts
Normal file
12
apps/builder/src/pages/api/mock/webhook-easy-config.ts
Normal 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)
|
27
apps/builder/src/pages/api/mock/webhook.ts
Normal file
27
apps/builder/src/pages/api/mock/webhook.ts
Normal 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)
|
19
apps/builder/src/pages/api/publicIdAvailable.ts
Normal file
19
apps/builder/src/pages/api/publicIdAvailable.ts
Normal 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)
|
42
apps/builder/src/pages/api/publicTypebots.ts
Normal file
42
apps/builder/src/pages/api/publicTypebots.ts
Normal 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)
|
50
apps/builder/src/pages/api/publicTypebots/[id].ts
Normal file
50
apps/builder/src/pages/api/publicTypebots/[id].ts
Normal 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)
|
39
apps/builder/src/pages/api/storage/upload-url.ts
Normal file
39
apps/builder/src/pages/api/storage/upload-url.ts
Normal 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)
|
42
apps/builder/src/pages/api/stripe/billing-portal.ts
Normal file
42
apps/builder/src/pages/api/stripe/billing-portal.ts
Normal 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)
|
61
apps/builder/src/pages/api/stripe/custom-plan-checkout.ts
Normal file
61
apps/builder/src/pages/api/stripe/custom-plan-checkout.ts
Normal 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)
|
49
apps/builder/src/pages/api/stripe/invoices.ts
Normal file
49
apps/builder/src/pages/api/stripe/invoices.ts
Normal 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)
|
260
apps/builder/src/pages/api/stripe/subscription.ts
Normal file
260
apps/builder/src/pages/api/stripe/subscription.ts
Normal 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)
|
132
apps/builder/src/pages/api/stripe/webhook.ts
Normal file
132
apps/builder/src/pages/api/stripe/webhook.ts
Normal 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))
|
115
apps/builder/src/pages/api/typebots.ts
Normal file
115
apps/builder/src/pages/api/typebots.ts
Normal 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)
|
88
apps/builder/src/pages/api/typebots/[typebotId].ts
Normal file
88
apps/builder/src/pages/api/typebots/[typebotId].ts
Normal 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)
|
22
apps/builder/src/pages/api/typebots/[typebotId]/blocks.ts
Normal file
22
apps/builder/src/pages/api/typebots/[typebotId]/blocks.ts
Normal 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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
67
apps/builder/src/pages/api/typebots/[typebotId]/results.ts
Normal file
67
apps/builder/src/pages/api/typebots/[typebotId]/results.ts
Normal 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)
|
@ -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)
|
@ -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)
|
@ -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)
|
26
apps/builder/src/pages/api/typebots/[typebotId]/webhooks.ts
Normal file
26
apps/builder/src/pages/api/typebots/[typebotId]/webhooks.ts
Normal 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)
|
23
apps/builder/src/pages/api/users/[userId].ts
Normal file
23
apps/builder/src/pages/api/users/[userId].ts
Normal 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)
|
40
apps/builder/src/pages/api/users/[userId]/api-tokens.ts
Normal file
40
apps/builder/src/pages/api/users/[userId]/api-tokens.ts
Normal 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)
|
@ -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)
|
71
apps/builder/src/pages/api/webhooks/[webhookId].ts
Normal file
71
apps/builder/src/pages/api/webhooks/[webhookId].ts
Normal 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)
|
36
apps/builder/src/pages/api/workspaces.ts
Normal file
36
apps/builder/src/pages/api/workspaces.ts
Normal 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)
|
41
apps/builder/src/pages/api/workspaces/[workspaceId].ts
Normal file
41
apps/builder/src/pages/api/workspaces/[workspaceId].ts
Normal 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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
54
apps/builder/src/pages/api/workspaces/[workspaceId]/usage.ts
Normal file
54
apps/builder/src/pages/api/workspaces/[workspaceId]/usage.ts
Normal 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)
|
Reference in New Issue
Block a user