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

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()
})
}
}, [])

6
apps/builder/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.

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"]
}

31
apps/viewer/.eslintrc.js Normal file
View File

@ -0,0 +1,31 @@
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',
},
}

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

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

21
apps/viewer/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"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",
"composite": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}