2
0

Add authentication

This commit is contained in:
Baptiste Arnaud
2021-11-29 15:19:07 +01:00
parent 68dd491eca
commit 5e14a94dea
51 changed files with 5036 additions and 90 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2

27
.env.example Normal file
View File

@ -0,0 +1,27 @@
DATABASE_URL=postgresql://username:password@host:5450/typebot?schema=public
# Used for email auth and email notifications
EMAIL_SERVER_USER=username
EMAIL_SERVER_PASSWORD=password
EMAIL_SERVER_HOST=smtp.example.com
EMAIL_SERVER_PORT=587
EMAIL_FROM=noreply@example.com
# AUTH
# (Optional) Used to login using GitHub
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# (Optional) Used to login using Google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# (Optional) Used to login using Facebook
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
# Used for uploading images, videos, etc...
S3_UPLOAD_KEY=
S3_UPLOAD_SECRET=
S3_UPLOAD_REGION=
S3_UPLOAD_BUCKET=

5
.gitignore vendored
View File

@ -24,5 +24,10 @@ node_modules
.next
.env
.env.local
cypress.env.json

View File

@ -1,6 +1,60 @@
# Getting Started
<div align="center">
<h1>Typebot</h1>
<a href="https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" /></a>
<a href="https://github.com/prisma/prisma/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL-blue" /></a>
<br />
<a href="https://docs.typebot.io">Quickstart</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="https://www.typebot.io/">Website</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="https://docs.typebot.io">Docs</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="https://www.typebot.io/blog">Blog</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="https://twitter.com/Typebot_io">Twitter</a>
<br />
<hr />
</div>
```sh
yarn set version berry
yarn install
```
## Development (localhost)
1. Clone the repo
```sh
git clone https://github.com/Typebot-io/typebot.git
```
2. Install packages with yarn
```sh
yarn set version berry
yarn install
```
3. Copy `.env.example` to `.env`
4. Configure environment variables in the `.env` file.
5. Setup the database
```sh
yarn dev:setup
```
6. Run the applications
```sh
yarn dev:builder
```
```sh
yarn dev:viewer
```
7. Open [Prisma Studio](https://www.prisma.io/studio) to look at or modify the database content
```sh
yarn db:inspect
```
## Deployment
TO-DO

32
apps/builder/.eslintrc.js Normal file
View File

@ -0,0 +1,32 @@
module.exports = {
ignorePatterns: ['node_modules'],
env: {
browser: true,
es6: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'next/core-web-vitals',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
plugins: ['prettier', 'react', 'cypress', '@typescript-eslint'],
rules: {
'react/no-unescaped-entities': [0],
'prettier/prettier': 'error',
'react/display-name': [0],
},
}

View File

@ -0,0 +1 @@
import { IconProps, Icon } from '@chakra-ui/react'

View File

@ -0,0 +1,69 @@
import { IconProps, Icon } from '@chakra-ui/react'
export const TypebotLogo = ({
isDark,
...props
}: { isDark?: boolean } & IconProps) => (
<Icon viewBox="0 0 500 500" w="50px" h="50px" {...props}>
<rect
width="500"
height="500"
rx="75"
fill={isDark ? 'white' : '#0042DA'}
/>
<rect
x="438.709"
y="170.968"
width="64.5161"
height="290.323"
rx="32.2581"
transform="rotate(90 438.709 170.968)"
fill="#FF8E20"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M93.5481 235.484C111.364 235.484 125.806 221.041 125.806 203.226C125.806 185.41 111.364 170.968 93.5481 170.968C75.7325 170.968 61.29 185.41 61.29 203.226C61.29 221.041 75.7325 235.484 93.5481 235.484Z"
fill="#FF8E20"
/>
<rect
x="61.29"
y="332.259"
width="64.5161"
height="290.323"
rx="32.2581"
transform="rotate(-90 61.29 332.259)"
fill={isDark ? '#0042DA' : 'white'}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M406.451 267.742C388.635 267.742 374.193 282.184 374.193 300C374.193 317.815 388.635 332.258 406.451 332.258C424.267 332.258 438.709 317.815 438.709 300C438.709 282.184 424.267 267.742 406.451 267.742Z"
fill={isDark ? '#0042DA' : 'white'}
/>
</Icon>
)
export const GithubLogo = (props: IconProps) => (
<Icon viewBox="0 0 512 512" {...props}>
<title>{'Logo Github'}</title>
<path d="M256 32C132.3 32 32 134.9 32 261.7c0 101.5 64.2 187.5 153.2 217.9a17.56 17.56 0 003.8.4c8.3 0 11.5-6.1 11.5-11.4 0-5.5-.2-19.9-.3-39.1a102.4 102.4 0 01-22.6 2.7c-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1a63 63 0 0025.6-6c2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6-2.3-5.8-10-29.2 2.2-60.8a18.64 18.64 0 015-.5c8.1 0 26.4 3.1 56.6 24.1a208.21 208.21 0 01112.2 0c30.2-21 48.5-24.1 56.6-24.1a18.64 18.64 0 015 .5c12.2 31.6 4.5 55 2.2 60.8 14.3 16.1 23 36.6 23 61.6 0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5a19.35 19.35 0 004-.4C415.9 449.2 480 363.1 480 261.7 480 134.9 379.7 32 256 32z" />
</Icon>
)
export const GoogleLogo = (props: IconProps) => (
<Icon viewBox="0 0 512 512" {...props}>
<title>{'Logo Google'}</title>
<path d="M473.16 221.48l-2.26-9.59H262.46v88.22H387c-12.93 61.4-72.93 93.72-121.94 93.72-35.66 0-73.25-15-98.13-39.11a140.08 140.08 0 01-41.8-98.88c0-37.16 16.7-74.33 41-98.78s61-38.13 97.49-38.13c41.79 0 71.74 22.19 82.94 32.31l62.69-62.36C390.86 72.72 340.34 32 261.6 32c-60.75 0-119 23.27-161.58 65.71C58 139.5 36.25 199.93 36.25 256s20.58 113.48 61.3 155.6c43.51 44.92 105.13 68.4 168.58 68.4 57.73 0 112.45-22.62 151.45-63.66 38.34-40.4 58.17-96.3 58.17-154.9 0-24.67-2.48-39.32-2.59-39.96z" />
</Icon>
)
export const FacebookLogo = (props: IconProps) => (
<Icon viewBox="0 0 512 512" {...props}>
<title>Logo Facebook</title>
<path
d="M480 257.35c0-123.7-100.3-224-224-224s-224 100.3-224 224c0 111.8 81.9 204.47 189 221.29V322.12h-56.89v-64.77H221V208c0-56.13 33.45-87.16 84.61-87.16 24.51 0 50.15 4.38 50.15 4.38v55.13H327.5c-27.81 0-36.51 17.26-36.51 35v42h62.12l-9.92 64.77H291v156.54c107.1-16.81 189-109.48 189-221.31z"
fillRule="evenodd"
/>
</Icon>
)

View File

@ -0,0 +1,81 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #0042da;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #0042da, 0 0 5px #0042da;
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #0042da;
border-left-color: #0042da;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,27 @@
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { User } from '@typebot/prisma'
export type withAuthProps = {
user?: User
}
const withAuth =
(WrappedComponent: ({ user }: withAuthProps) => JSX.Element) =>
(props: JSX.IntrinsicAttributes & withAuthProps) => {
const router = useRouter()
const { data: session, status } = useSession()
useEffect(() => {
if (!router.isReady) return
if (status === 'loading') return
if (status === 'unauthenticated') router.replace('/signin')
}, [status, router])
return (
<WrappedComponent user={session?.user as User | undefined} {...props} />
)
}
export default withAuth

View File

@ -0,0 +1,33 @@
import Head from 'next/head'
export const Seo = ({
title,
currentUrl = 'https://app.typebot.io',
description = 'Create and publish conversational forms that collect 4 times more answers and feel native to your product',
imagePreviewUrl = 'https://app.typebot.io/site-preview.png',
}: {
title: string
description?: string
currentUrl?: string
imagePreviewUrl?: string
}) => (
<Head>
<title>Typebot | {title}</title>
<meta name="title" content={title} />
<meta property="og:title" content={title} />
<meta property="twitter:title" content={title} />
<meta property="twitter:url" content={currentUrl} />
<meta property="og:url" content={currentUrl} />
<meta name="description" content={description} />
<meta property="twitter:description" content={description} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imagePreviewUrl} />
<meta property="twitter:image" content={imagePreviewUrl} />
<meta property="og:type" content="website" />
<meta property="twitter:card" content="summary_large_image" />
</Head>
)

View File

@ -0,0 +1,26 @@
import React from 'react'
import { NextChakraLink } from '../nextChakra/NextChakraLink'
import { Text } from '@chakra-ui/react'
type Props = {
type: 'register' | 'signin'
}
export const AuthSwitcher = ({ type }: Props) => (
<>
{type === 'signin' ? (
<Text>
Don't have an account?{' '}
<NextChakraLink href="/register" color="blue.500" textDecor="underline">
Sign up for free
</NextChakraLink>
</Text>
) : (
<Text>
Already have an account?{' '}
<NextChakraLink href="/signin" color="blue.500" textDecor="underline">
Sign in
</NextChakraLink>
</Text>
)}
</>
)

View File

@ -0,0 +1,25 @@
import { FlexProps, Flex, Box, Divider, Text } from '@chakra-ui/react'
import { useColorModeValue } from '@chakra-ui/system'
import React from 'react'
export const DividerWithText = (props: FlexProps) => {
const { children, ...flexProps } = props
return (
<Flex align="center" color="gray.300" {...flexProps}>
<Box flex="1">
<Divider borderColor="currentcolor" />
</Box>
<Text
as="span"
px="3"
color={useColorModeValue('gray.600', 'gray.400')}
fontWeight="medium"
>
{children}
</Text>
<Box flex="1">
<Divider borderColor="currentcolor" />
</Box>
</Flex>
)
}

View File

@ -0,0 +1,76 @@
import {
Button,
HTMLChakraProps,
Input,
Stack,
HStack,
useToast,
} from '@chakra-ui/react'
import React, { ChangeEvent, FormEvent, useEffect } from 'react'
import { useState } from 'react'
import { signIn, useSession } from 'next-auth/react'
import { DividerWithText } from './DividerWithText'
import { SocialLoginButtons } from './SocialLoginButtons'
import { useRouter } from 'next/router'
type Props = {
defaultEmail?: string
}
export const SignInForm = ({
defaultEmail,
}: Props & HTMLChakraProps<'form'>) => {
const router = useRouter()
const { status } = useSession()
const [authLoading, setAuthLoading] = useState(false)
const [emailValue, setEmailValue] = useState(defaultEmail ?? '')
const toast = useToast({
position: 'top-right',
})
useEffect(() => {
if (status === 'authenticated') router.replace('/typebots')
}, [status, router])
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) =>
setEmailValue(e.target.value)
const handleEmailSubmit = async (e: FormEvent) => {
e.preventDefault()
setAuthLoading(true)
await signIn('email', {
email: emailValue,
redirect: false,
})
toast({
status: 'success',
title: 'Success!',
description: 'Check your inbox to sign in',
})
setAuthLoading(false)
}
return (
<Stack spacing="4">
<SocialLoginButtons />
<DividerWithText mt="6">Or with your email</DividerWithText>
<HStack as="form" onSubmit={handleEmailSubmit}>
<Input
name="email"
type="email"
autoComplete="email"
placeholder="email@company.com"
required
value={emailValue}
onChange={handleEmailChange}
/>
<Button
type="submit"
isLoading={
['loading', 'authenticated'].includes(status) || authLoading
}
>
Submit
</Button>
</HStack>
</Stack>
)
}

View File

@ -0,0 +1,46 @@
import { FacebookLogo, GithubLogo, GoogleLogo } from 'assets/logos'
import { Stack, Button } from '@chakra-ui/react'
import { signIn, useSession } from 'next-auth/react'
import React from 'react'
export const SocialLoginButtons = () => {
const { status } = useSession()
const handleGitHubClick = async () => signIn('github')
const handleGoogleClick = async () => signIn('google')
const handleFacebookClick = async () => signIn('facebook')
return (
<Stack>
<Button
leftIcon={<GithubLogo />}
colorScheme="gray"
onClick={handleGitHubClick}
data-testid="github"
isLoading={['loading', 'authenticated'].includes(status)}
>
Continue with GitHub
</Button>
<Button
leftIcon={<GoogleLogo />}
colorScheme="gray"
onClick={handleGoogleClick}
data-testid="google"
isLoading={['loading', 'authenticated'].includes(status)}
>
Continue with Google
</Button>
<Button
leftIcon={<FacebookLogo />}
colorScheme="gray"
onClick={handleFacebookClick}
data-testid="facebook"
isLoading={['loading', 'authenticated'].includes(status)}
>
Continue with Facebook
</Button>
</Stack>
)
}

View File

@ -0,0 +1,47 @@
import { PropsWithChildren } from 'react'
import NextLink from 'next/link'
import { LinkProps as NextLinkProps } from 'next/dist/client/link'
import {
Link as ChakraLink,
LinkProps as ChakraLinkProps,
} from '@chakra-ui/react'
import React from 'react'
export type NextChakraLinkProps = PropsWithChildren<
NextLinkProps & Omit<ChakraLinkProps, 'href'>
>
export const NextChakraLink = React.forwardRef<
HTMLAnchorElement,
NextChakraLinkProps
>(
(
{
href,
replace,
scroll,
shallow,
prefetch,
children,
locale,
...chakraProps
},
ref
) => {
return (
<NextLink
passHref={true}
href={href}
replace={replace}
scroll={scroll}
shallow={shallow}
prefetch={prefetch}
locale={locale}
>
<ChakraLink ref={ref} {...chakraProps}>
{children}
</ChakraLink>
</NextLink>
)
}
)

View File

@ -0,0 +1,7 @@
{
"baseUrl": "http://localhost:3000",
"chromeWebSecurity": false,
"integrationFolder": "cypress/tests",
"viewportWidth": 1400,
"viewportHeight": 800
}

View File

@ -0,0 +1,20 @@
import {
GitHubSocialLogin,
FacebookSocialLogin,
GoogleSocialLogin,
} from 'cypress-social-logins/src/Plugins'
/// <reference types="cypress" />
/**
* @type {Cypress.PluginConfig}
*/
const handler = (on: any) => {
on('task', {
GoogleSocialLogin: GoogleSocialLogin,
FacebookSocialLogin: FacebookSocialLogin,
GitHubSocialLogin: GitHubSocialLogin,
})
}
export default handler

View File

@ -0,0 +1,37 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import '@testing-library/cypress/add-commands'
Cypress.Commands.add('logOutByApi', () =>
cy
.request('GET', `${Cypress.env('SITE_NAME')}/api/auth/csrf/login`)
.its('body')
.then((result) => {
cy.request('POST', `${Cypress.env('SITE_NAME')}/api/auth/signout`, {
csrfToken: result.csrfToken,
})
})
)

View File

@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-namespace */
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
declare global {
namespace Cypress {
interface Chainable {
/**
* Log out using the NextAuth API.
* @example cy.logOutByApi()
*/
logOutByApi(): Chainable<Response<any>>
}
}
}
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,103 @@
describe('SignIn page', () => {
beforeEach(() => {
cy.logOutByApi()
})
it('can continue with Google', () => {
cy.visit('/signin')
const username = Cypress.env('GOOGLE_USER')
const password = Cypress.env('GOOGLE_PW')
const loginUrl = Cypress.env('SITE_NAME')
const cookieName = Cypress.env('COOKIE_NAME')
exectueSocialLogin(
'GoogleSocialLogin',
username,
password,
loginUrl,
cookieName
)
})
it('can continue with GitHub', () => {
cy.visit('/signin')
const username = Cypress.env('GITHUB_USER')
const password = Cypress.env('GITHUB_PW')
const loginUrl = Cypress.env('SITE_NAME')
const cookieName = Cypress.env('COOKIE_NAME')
exectueSocialLogin(
'GitHubSocialLogin',
username,
password,
loginUrl,
cookieName
)
})
it('can continue with Facebook', () => {
cy.visit('/signin')
const username = Cypress.env('FACEBOOK_USER')
const password = Cypress.env('FACEBOOK_PW')
const loginUrl = Cypress.env('SITE_NAME')
const cookieName = Cypress.env('COOKIE_NAME')
exectueSocialLogin(
'FacebookSocialLogin',
username,
password,
loginUrl,
cookieName,
[
'button[data-testid="cookie-policy-dialog-manage-button"]',
'button[data-testid="cookie-policy-manage-dialog-accept-button"]',
]
)
})
// We don't test email sign in because disabling email sending is not straightforward
})
const exectueSocialLogin = (
task: 'FacebookSocialLogin' | 'GoogleSocialLogin' | 'GitHubSocialLogin',
username: string,
password: string,
loginUrl: string,
cookieName: string,
trackingConsentSelectors?: string[]
) => {
const selectorId =
task === 'FacebookSocialLogin'
? 'facebook'
: task === 'GoogleSocialLogin'
? 'google'
: 'github'
const socialLoginOptions = {
username,
password,
loginUrl,
headless: true,
logs: true,
isPopup: false,
loginSelector: `[data-testid="${selectorId}"]`,
postLoginSelector: `[data-testid="authenticated"]`,
trackingConsentSelectors,
}
cy.task(task, socialLoginOptions).then(({ cookies }: any) => {
const cookie = cookies
.filter((cookie: any) => cookie.name === cookieName)
.pop()
if (cookie) {
cy.setCookie(cookie.name, cookie.value, {
domain: cookie.domain,
expiry: cookie.expires,
httpOnly: cookie.httpOnly,
path: cookie.path,
secure: cookie.secure,
})
Cypress.Cookies.defaults({
preserve: cookieName,
})
}
cy.visit('/typebots')
cy.findByText(`Hello ${username}`).should('exist')
})
}

View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts"],
"exclude": [],
"compilerOptions": {
"types": ["cypress", "@testing-library/cypress"],
"lib": ["es2015", "dom"],
"target": "es5",
"isolatedModules": false,
"allowJs": true,
"noEmit": true
}
}

View File

@ -0,0 +1,11 @@
import { TypebotLogo } from 'assets/logos'
import { Spinner, VStack } from '@chakra-ui/react'
export const LoadingPage = () => (
<div className="flex h-screen items-center justify-center">
<VStack spacing={6}>
<TypebotLogo boxSize="80px" />
<Spinner />
</VStack>
</div>
)

View File

@ -0,0 +1,83 @@
import { extendTheme } from '@chakra-ui/react'
const fonts = {
heading: 'Outfit',
body: 'Open Sans',
}
const colors = {
blue: {
50: '#e0edff',
100: '#b0caff',
200: '#7ea6ff',
300: '#4b83ff',
400: '#1a5fff',
500: '#0042da',
600: '#0036b4',
700: '#002782',
800: '#001751',
900: '#1a202c',
},
orange: {
50: '#fff1da',
100: '#ffd7ae',
200: '#ffbf7d',
300: '#ffa54c',
400: '#ff8b1a',
500: '#e67200',
600: '#b45800',
700: '#813e00',
800: '#4f2500',
900: '#200b00',
},
yellow: {
50: '#fff9da',
100: '#ffedad',
200: '#ffe17d',
300: '#ffd54b',
400: '#ffc91a',
500: '#e6b000',
600: '#b38800',
700: '#806200',
800: '#4e3a00',
900: '#1d1400',
},
}
const components = {
Spinner: {
defaultProps: {
colorScheme: 'blue',
},
},
Button: {
defaultProps: {
colorScheme: 'blue',
},
},
NumberInput: {
defaultProps: {
focusBorderColor: 'blue.200',
},
},
Input: {
defaultProps: {
focusBorderColor: 'blue.200',
},
},
Popover: {
baseStyle: {
popper: {
width: 'fit-content',
maxWidth: 'fit-content',
},
},
},
Link: {
baseStyle: {
_hover: { textDecoration: 'none' },
},
},
}
export const customTheme = extendTheme({ colors, fonts, components })

View File

@ -0,0 +1,22 @@
import Router from 'next/router'
import NProgress from 'nprogress'
import { useEffect } from 'react'
export const useRouterProgressBar = () =>
useEffect(() => {
if (typeof window !== 'undefined') {
NProgress.configure({ showSpinner: false })
Router.events.on('routeChangeStart', () => {
NProgress.start()
})
Router.events.on('routeChangeComplete', () => {
NProgress.done()
})
Router.events.on('routeChangeError', () => {
NProgress.done()
})
}
}, [])

42
apps/builder/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "builder",
"packageManager": "yarn@3.1.0",
"scripts": {
"dev": "dotenv -e ../../.env next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"cypress": "cypress open"
},
"dependencies": {
"@chakra-ui/react": "^1.7.2",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@next-auth/prisma-adapter": "next",
"framer-motion": "^4",
"next": "^12.0.4",
"next-auth": "beta",
"nodemailer": "^6.7.1",
"nprogress": "^0.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@testing-library/cypress": "^8.0.2",
"@types/node": "^16.11.9",
"@types/nprogress": "^0.2.0",
"@types/react": "^17.0.35",
"@types/testing-library__cypress": "^5.0.9",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"cypress": "^9.1.0",
"cypress-social-logins": "^1.12.0",
"dotenv-cli": "^4.1.0",
"eslint": "<8.0.0",
"eslint-config-next": "12.0.4",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.4.1",
"typescript": "^4.5.2"
}
}

View File

@ -0,0 +1,21 @@
import React from 'react'
import { AppProps } from 'next/app'
import { SessionProvider } from 'next-auth/react'
import { ChakraProvider } from '@chakra-ui/react'
import { customTheme } from 'libs/chakra'
import 'assets/styles/routerProgressBar.css'
import { useRouterProgressBar } from 'libs/routerProgressBar'
const App = ({ Component, pageProps }: AppProps) => {
useRouterProgressBar()
return (
<ChakraProvider theme={customTheme}>
<SessionProvider>
<Component {...pageProps} />
</SessionProvider>
</ChakraProvider>
)
}
export default App

View File

@ -0,0 +1,36 @@
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html>
<Head>
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

View File

@ -0,0 +1,39 @@
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { PrismaClient } from '@typebot/prisma'
import EmailProvider from 'next-auth/providers/email'
import GitHubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import FacebookProvider from 'next-auth/providers/facebook'
const prisma = new PrismaClient()
export default NextAuth({
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET,
providers: [
EmailProvider({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
},
from: process.env.EMAIL_FROM,
}),
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID ?? '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID ?? '',
clientSecret: process.env.FACEBOOK_CLIENT_SECRET ?? '',
}),
],
})

View File

@ -0,0 +1,18 @@
import { GetServerSidePropsContext } from 'next'
import { getSession } from 'next-auth/react'
function RedirectPage() {
return
}
export const getServerSideProps = async (
context: GetServerSidePropsContext
) => {
const session = await getSession(context)
if (!session?.user) {
return { redirect: { permanent: false, destination: '/signin' } }
}
return { redirect: { permanent: false, destination: '/typebots' } }
}
export default RedirectPage

View File

@ -0,0 +1,19 @@
import { AuthSwitcher } from 'components/auth/AuthSwitcher'
import { SignInForm } from 'components/auth/SignInForm'
import { Heading, VStack } from '@chakra-ui/react'
import { useRouter } from 'next/router'
import React from 'react'
const RegisterPage = () => {
const { query } = useRouter()
return (
<VStack spacing={4} h="100vh" justifyContent="center">
<Heading>Create an account</Heading>
<AuthSwitcher type="register" />
<SignInForm defaultEmail={query.g?.toString()} />
</VStack>
)
}
export default RegisterPage

View File

@ -0,0 +1,16 @@
import { AuthSwitcher } from 'components/auth/AuthSwitcher'
import { SignInForm } from 'components/auth/SignInForm'
import { Heading, VStack } from '@chakra-ui/react'
import React from 'react'
const SignInPage = () => {
return (
<VStack spacing={4} h="100vh" justifyContent="center">
<Heading>Sign in</Heading>
<AuthSwitcher type="signin" />
<SignInForm />
</VStack>
)
}
export default SignInPage

View File

@ -0,0 +1,9 @@
import withAuth, { withAuthProps } from 'components/HOC/withUser'
import { Text } from '@chakra-ui/react'
import React from 'react'
const TypebotsPage = ({ user }: withAuthProps) => {
return <Text data-testid="authenticated">Hello {user?.email}</Text>
}
export default withAuth(TypebotsPage)

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"composite": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "cypress"]
}

6
apps/viewer/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

27
apps/viewer/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "viewer",
"packageManager": "yarn@3.1.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^12.0.4",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/node": "^16.11.9",
"@types/react": "^17.0.35",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"eslint": "<8.0.0",
"eslint-config-next": "12.0.4",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.4.1",
"typescript": "^4.5.2"
}
}

View File

@ -1,7 +1,7 @@
import React from 'react'
const HomePage = () => {
return <div>Welcome to "Next.js"!</div>
return <div>Welcome to "Viewer"!</div>
}
export default HomePage

View File

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
@ -17,14 +13,9 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
"jsx": "preserve",
"composite": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
version: '3.6'
services:
postgres:
image: postgres:13
ports:
- '5432:5432'
restart: always
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:

View File

@ -1,26 +1,22 @@
{
"name": "typebot-os",
"packageManager": "yarn@3.1.0",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^12.0.4",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"db:up": "docker-compose up -d",
"db:setup": "yarn workspace @typebot/prisma dev",
"db:nuke": "docker-compose down --volumes --remove-orphans",
"db:inspect": "dotenv -e .env yarn workspace @typebot/prisma prisma studio",
"dev:setup": "dotenv -e .env run-s db:up db:setup",
"dev:builder": "yarn workspace builder dev",
"dev:viewer": "yarn workspace viewer dev"
},
"devDependencies": {
"@types/node": "^16.11.9",
"@types/react": "^17.0.35",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"eslint": "<8.0.0",
"eslint-config-next": "12.0.4",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.4.1",
"typescript": "^4.5.2"
"dotenv-cli": "^4.1.0",
"npm-run-all": "^4.1.5"
}
}

5
packages/prisma/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env
build

View File

@ -0,0 +1 @@
# schemas

1
packages/prisma/index.ts Normal file
View File

@ -0,0 +1 @@
export * from '@prisma/client'

View File

@ -0,0 +1,22 @@
{
"name": "@typebot/prisma",
"packageManager": "yarn@3.1.0",
"devDependencies": {
"dotenv-cli": "^4.1.0",
"npm-run-all": "^4.1.5",
"prisma": "^3.5.0",
"ts-node": "^10.4.0",
"typescript": "^4.5.2"
},
"dependencies": {
"@prisma/client": "^3.5.0"
},
"scripts": {
"prisma": "dotenv -e ../../.env prisma",
"dev": "run-s migrate generate build",
"build": "dotenv -e ../../.env tsc --build",
"migrate": "dotenv -e ../../.env prisma migrate dev",
"push": "dotenv -e ../../.env prisma db push",
"generate": "dotenv -e ../../.env prisma generate"
}
}

View File

@ -0,0 +1,69 @@
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"oauth_token_secret" TEXT,
"oauth_token" TEXT,
"refresh_token_expires_in" INTEGER,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -0,0 +1,56 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
oauth_token_secret String?
oauth_token String?
refresh_token_expires_in Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"composite": true,
"outDir": "build",
"isolatedModules": false
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "build"]
}

60
workspace.code-workspace Normal file
View File

@ -0,0 +1,60 @@
{
"folders": [
{
"path": ".",
"name": "root"
},
{
"path": "apps/builder"
},
{
"path": "apps/viewer"
},
{
"path": "packages/bot-engine"
},
{
"path": "packages/prisma"
}
],
"settings": {
"restoreTerminals.terminals": [
{
"splitTerminals": [
{
"name": "root",
"commands": ["cd ~/Dev/typebot-os"]
}
]
},
{
"splitTerminals": [
{
"name": "builder",
"commands": ["cd ~/Dev/typebot-os/apps/builder"]
},
{
"name": "cypress",
"commands": ["cd ~/Dev/typebot-os/apps/builder"]
}
]
},
{
"splitTerminals": [
{
"name": "bot-engine",
"commands": ["cd ~/Dev/typebot-os/packages/bot-engine"]
}
]
},
{
"splitTerminals": [
{
"name": "viewer",
"commands": ["cd ~/Dev/typebot-os/apps/viewer"]
}
]
}
]
}
}

3675
yarn.lock

File diff suppressed because it is too large Load Diff