2
0

Add user account page

This commit is contained in:
Baptiste Arnaud
2021-12-27 15:59:32 +01:00
parent 698867da5d
commit e10fe1a186
33 changed files with 911 additions and 129 deletions

View File

@ -0,0 +1,18 @@
import { Stack } from '@chakra-ui/react'
import { AccountHeader } from 'components/account/AccountHeader'
import { Seo } from 'components/Seo'
import { UserContext } from 'contexts/UserContext'
import { AccountContent } from 'layouts/account/AccountContent'
const AccountSubscriptionPage = () => {
return (
<UserContext>
<Seo title="My account" />{' '}
<Stack>
<AccountHeader />
<AccountContent />
</Stack>
</UserContext>
)
}
export default AccountSubscriptionPage

View File

@ -1,4 +1,4 @@
import NextAuth from 'next-auth'
import NextAuth, { NextAuthOptions } from 'next-auth'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import EmailProvider from 'next-auth/providers/email'
import GitHubProvider from 'next-auth/providers/github'
@ -8,6 +8,7 @@ import CredentialsProvider from 'next-auth/providers/credentials'
import prisma from 'libs/prisma'
import { Provider } from 'next-auth/providers'
import { User } from 'db'
import { NextApiRequest, NextApiResponse } from 'next'
const providers: Provider[] = [
EmailProvider({
@ -67,7 +68,7 @@ if (process.env.NODE_ENV !== 'production')
})
)
export default NextAuth({
const createOptions = (req: NextApiRequest): NextAuthOptions => ({
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET,
providers,
@ -75,13 +76,25 @@ export default NextAuth({
strategy: process.env.NODE_ENV === 'production' ? 'database' : 'jwt',
},
callbacks: {
jwt: async ({ token, user }) => {
user && (token.user = user)
jwt: async ({ token, user, account }) => {
if (req.url === '/api/auth/session?update' && token.user) {
token.user = await prisma.user.findUnique({
where: { id: (token.user as User).id },
})
} else if (user) {
token.user = user
}
account?.type && (token.providerType = account?.type)
return token
},
session: async ({ session, token, user }) => {
token?.user ? (session.user = token.user as User) : (session.user = user)
return session
return { ...session, providerType: token.providerType }
},
},
})
const handler = (req: NextApiRequest, res: NextApiResponse) => {
NextAuth(req, res, createOptions(req))
}
export default handler

View File

@ -0,0 +1,46 @@
import aws from 'aws-sdk'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { methodNotAllowed } from 'services/api/utils'
const maxUploadFileSize = 10485760 // 10 MB
const handler = async (
req: NextApiRequest,
res: NextApiResponse
): Promise<void> => {
try {
res.setHeader('Access-Control-Allow-Origin', '*')
if (req.method === 'GET') {
const session = await getSession({ req })
if (!session) {
res.status(401)
return
}
aws.config.update({
accessKeyId: process.env.S3_UPLOAD_KEY,
secretAccessKey: process.env.S3_UPLOAD_SECRET,
region: process.env.S3_UPLOAD_REGION,
signatureVersion: 'v4',
})
const s3 = new aws.S3()
const post = s3.createPresignedPost({
Bucket: process.env.S3_UPLOAD_BUCKET,
Fields: {
ACL: 'public-read',
key: req.query.key,
'Content-Type': req.query.fileType,
},
Expires: 120, // seconds
Conditions: [['content-length-range', 0, maxUploadFileSize]],
})
return res.status(200).json(post)
}
return methodNotAllowed(res)
} catch (err) {
console.log(err)
}
}
export default handler

View File

@ -0,0 +1,35 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'services/api/utils'
import Stripe from 'stripe'
const usdPriceIdTest = 'price_1Jc4TQKexUFvKTWyGvsH4Ff5'
const createCheckoutSession = async (
req: NextApiRequest,
res: NextApiResponse
) => {
if (req.method === 'POST') {
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2020-08-27',
})
const { email } = req.body
const session = await stripe.checkout.sessions.create({
success_url: `${req.headers.origin}/typebots?stripe=success`,
cancel_url: `${req.headers.origin}/typebots?stripe=cancel`,
automatic_tax: { enabled: true },
allow_promotion_codes: true,
customer_email: email,
line_items: [
{
price: usdPriceIdTest,
quantity: 1,
},
],
})
res.status(201).json(session)
}
return methodNotAllowed(res)
}
export default createCheckoutSession

View File

@ -0,0 +1,73 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'services/api/utils'
import Stripe from 'stripe'
import Cors from 'micro-cors'
import { buffer } from 'micro'
import prisma from 'libs/prisma'
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: '2020-08-27',
})
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 { customer_email } = session
if (!customer_email)
return res.status(500).send(`customer_email not found`)
await prisma.user.update({
where: { email: customer_email },
data: { plan: Plan.PRO, stripeId: session.customer as string },
})
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
await prisma.user.update({
where: {
stripeId: subscription.customer as string,
},
data: {
plan: Plan.FREE,
},
})
}
}
} catch (err) {
if (err instanceof Error) {
console.error(err)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
}
}
return methodNotAllowed(res)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default cors(webhookHandler as any)

View File

@ -0,0 +1,24 @@
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { methodNotAllowed } from 'services/api/utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })
const id = req.query.id.toString()
if (req.method === 'PUT') {
const data = JSON.parse(req.body)
const typebots = await prisma.user.update({
where: { id },
data,
})
return res.send({ typebots })
}
return methodNotAllowed(res)
}
export default handler

View File

@ -1,18 +1,20 @@
import withAuth from 'components/HOC/withUser'
import React from 'react'
import { Stack } from '@chakra-ui/layout'
import { DashboardHeader } from 'components/dashboard/DashboardHeader'
import { Seo } from 'components/Seo'
import { FolderContent } from 'components/dashboard/FolderContent'
import { UserContext } from 'contexts/UserContext'
const DashboardPage = () => {
return (
<Stack>
<UserContext>
<Seo title="My typebots" />
<DashboardHeader />
<FolderContent folder={null} />
</Stack>
<Stack>
<DashboardHeader />
<FolderContent folder={null} />
</Stack>
</UserContext>
)
}
export default withAuth(DashboardPage)
export default DashboardPage

View File

@ -1,6 +1,5 @@
import { Flex } from '@chakra-ui/layout'
import { Board } from 'components/board/Board'
import withAuth from 'components/HOC/withUser'
import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader'
import { EditorContext } from 'contexts/EditorContext'
@ -11,25 +10,28 @@ import { KBarProvider } from 'kbar'
import React from 'react'
import { actions } from 'libs/kbar'
import { KBar } from 'components/shared/KBar'
import { UserContext } from 'contexts/UserContext'
const TypebotEditPage = () => {
const { query } = useRouter()
return (
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Editor" />
<EditorContext>
<KBarProvider actions={actions}>
<KBar />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<GraphProvider>
<Board />
</GraphProvider>
</Flex>
</KBarProvider>
</EditorContext>
</TypebotContext>
<UserContext>
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Editor" />
<EditorContext>
<KBarProvider actions={actions}>
<KBar />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<GraphProvider>
<Board />
</GraphProvider>
</Flex>
</KBarProvider>
</EditorContext>
</TypebotContext>
</UserContext>
)
}
export default withAuth(TypebotEditPage)
export default TypebotEditPage

View File

@ -1,23 +1,25 @@
import { Flex } from '@chakra-ui/layout'
import withAuth from 'components/HOC/withUser'
import { ResultsContent } from 'layouts/results/ResultsContent'
import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader'
import { TypebotContext } from 'contexts/TypebotContext'
import { useRouter } from 'next/router'
import React from 'react'
import { UserContext } from 'contexts/UserContext'
const ResultsPage = () => {
const { query } = useRouter()
return (
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Share" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ResultsContent />
</Flex>
</TypebotContext>
<UserContext>
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Share" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ResultsContent />
</Flex>
</TypebotContext>
</UserContext>
)
}
export default withAuth(ResultsPage)
export default ResultsPage

View File

@ -1,23 +1,25 @@
import { Flex } from '@chakra-ui/layout'
import withAuth from 'components/HOC/withUser'
import { ResultsContent } from 'layouts/results/ResultsContent'
import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader'
import { TypebotContext } from 'contexts/TypebotContext'
import { useRouter } from 'next/router'
import React from 'react'
import { UserContext } from 'contexts/UserContext'
const AnalyticsPage = () => {
const { query } = useRouter()
return (
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Analytics" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ResultsContent />
</Flex>
</TypebotContext>
<UserContext>
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Analytics" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ResultsContent />
</Flex>
</TypebotContext>
</UserContext>
)
}
export default withAuth(AnalyticsPage)
export default AnalyticsPage

View File

@ -1,23 +1,25 @@
import { Flex } from '@chakra-ui/layout'
import withAuth from 'components/HOC/withUser'
import { Seo } from 'components/Seo'
import { SettingsContent } from 'components/settings/SettingsContent'
import { TypebotHeader } from 'components/shared/TypebotHeader'
import { TypebotContext } from 'contexts/TypebotContext'
import { UserContext } from 'contexts/UserContext'
import { useRouter } from 'next/router'
import React from 'react'
const SettingsPage = () => {
const { query } = useRouter()
return (
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Settings" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<SettingsContent />
</Flex>
</TypebotContext>
<UserContext>
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Settings" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<SettingsContent />
</Flex>
</TypebotContext>
</UserContext>
)
}
export default withAuth(SettingsPage)
export default SettingsPage

View File

@ -1,23 +1,25 @@
import { Flex } from '@chakra-ui/layout'
import withAuth from 'components/HOC/withUser'
import { Seo } from 'components/Seo'
import { ShareContent } from 'components/share/ShareContent'
import { TypebotHeader } from 'components/shared/TypebotHeader'
import { TypebotContext } from 'contexts/TypebotContext'
import { UserContext } from 'contexts/UserContext'
import { useRouter } from 'next/router'
import React from 'react'
const SharePage = () => {
const { query } = useRouter()
return (
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Share" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ShareContent />
</Flex>
</TypebotContext>
<UserContext>
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Share" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ShareContent />
</Flex>
</TypebotContext>
</UserContext>
)
}
export default withAuth(SharePage)
export default SharePage

View File

@ -1,23 +1,25 @@
import { Flex } from '@chakra-ui/layout'
import withAuth from 'components/HOC/withUser'
import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader'
import { ThemeContent } from 'components/theme/ThemeContent'
import { TypebotContext } from 'contexts/TypebotContext'
import { UserContext } from 'contexts/UserContext'
import { useRouter } from 'next/router'
import React from 'react'
const ThemePage = () => {
const { query } = useRouter()
return (
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Theme" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ThemeContent />
</Flex>
</TypebotContext>
<UserContext>
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Theme" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ThemeContent />
</Flex>
</TypebotContext>
</UserContext>
)
}
export default withAuth(ThemePage)
export default ThemePage

View File

@ -1,14 +1,13 @@
import React, { useState } from 'react'
import { Button, Stack, useToast } from '@chakra-ui/react'
import { useUser } from 'services/user'
import { useRouter } from 'next/router'
import { Seo } from 'components/Seo'
import { DashboardHeader } from 'components/dashboard/DashboardHeader'
import { createTypebot } from 'services/typebots'
import withAuth from 'components/HOC/withUser'
import { UserContext, useUser } from 'contexts/UserContext'
const TemplatesPage = () => {
const user = useUser()
const { user } = useUser()
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
@ -31,14 +30,20 @@ const TemplatesPage = () => {
}
return (
<Stack>
<UserContext>
<Seo title="Templates" />
<DashboardHeader />
<Button ml={4} onClick={() => handleCreateSubmit()} isLoading={isLoading}>
Start from scratch
</Button>
</Stack>
<Stack>
<DashboardHeader />
<Button
ml={4}
onClick={() => handleCreateSubmit()}
isLoading={isLoading}
>
Start from scratch
</Button>
</Stack>
</UserContext>
)
}
export default withAuth(TemplatesPage)
export default TemplatesPage

View File

@ -1,4 +1,3 @@
import withAuth from 'components/HOC/withUser'
import React from 'react'
import { Flex, Stack } from '@chakra-ui/layout'
import { DashboardHeader } from 'components/dashboard/DashboardHeader'
@ -7,6 +6,7 @@ import { FolderContent } from 'components/dashboard/FolderContent'
import { useRouter } from 'next/router'
import { useFolderContent } from 'services/folders'
import { Spinner, useToast } from '@chakra-ui/react'
import { UserContext } from 'contexts/UserContext'
const FolderPage = () => {
const router = useRouter()
@ -27,18 +27,20 @@ const FolderPage = () => {
})
return (
<Stack>
<UserContext>
<Seo title="My typebots" />
<DashboardHeader />
{!folder ? (
<Flex flex="1">
<Spinner mx="auto" />
</Flex>
) : (
<FolderContent folder={folder} />
)}
</Stack>
<Stack>
<DashboardHeader />
{!folder ? (
<Flex flex="1">
<Spinner mx="auto" />
</Flex>
) : (
<FolderContent folder={folder} />
)}
</Stack>
</UserContext>
)
}
export default withAuth(FolderPage)
export default FolderPage