♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
14
apps/builder/src/features/auth/api/getAuthenticatedUser.ts
Normal file
14
apps/builder/src/features/auth/api/getAuthenticatedUser.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { setUser } from '@sentry/nextjs'
|
||||
import { User } from 'db'
|
||||
import { NextApiRequest } from 'next'
|
||||
import { getSession } from 'next-auth/react'
|
||||
|
||||
export const getAuthenticatedUser = async (
|
||||
req: NextApiRequest
|
||||
): Promise<User | undefined> => {
|
||||
const session = await getSession({ req })
|
||||
if (!session?.user || !('id' in session.user)) return
|
||||
const user = session.user as User
|
||||
setUser({ id: user.id, email: user.email ?? undefined })
|
||||
return session?.user as User
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import {
|
||||
FlexProps,
|
||||
Flex,
|
||||
Box,
|
||||
Divider,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
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>
|
||||
)
|
||||
}
|
122
apps/builder/src/features/auth/components/SignInForm.tsx
Normal file
122
apps/builder/src/features/auth/components/SignInForm.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import {
|
||||
Button,
|
||||
HTMLChakraProps,
|
||||
Input,
|
||||
Stack,
|
||||
HStack,
|
||||
Text,
|
||||
Spinner,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { ChangeEvent, FormEvent, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
ClientSafeProvider,
|
||||
getProviders,
|
||||
LiteralUnion,
|
||||
signIn,
|
||||
useSession,
|
||||
} from 'next-auth/react'
|
||||
import { DividerWithText } from './DividerWithText'
|
||||
import { SocialLoginButtons } from './SocialLoginButtons'
|
||||
import { useRouter } from 'next/router'
|
||||
import { BuiltInProviderType } from 'next-auth/providers'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
|
||||
type Props = {
|
||||
defaultEmail?: string
|
||||
}
|
||||
export const SignInForm = ({
|
||||
defaultEmail,
|
||||
}: Props & HTMLChakraProps<'form'>) => {
|
||||
const router = useRouter()
|
||||
const { status } = useSession()
|
||||
const [authLoading, setAuthLoading] = useState(false)
|
||||
const [isLoadingProviders, setIsLoadingProviders] = useState(true)
|
||||
|
||||
const [emailValue, setEmailValue] = useState(defaultEmail ?? '')
|
||||
const { showToast } = useToast()
|
||||
const [providers, setProviders] =
|
||||
useState<
|
||||
Record<LiteralUnion<BuiltInProviderType, string>, ClientSafeProvider>
|
||||
>()
|
||||
|
||||
const hasNoAuthProvider =
|
||||
!isLoadingProviders && Object.keys(providers ?? {}).length === 0
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'authenticated')
|
||||
router.replace({ pathname: '/typebots', query: router.query })
|
||||
;(async () => {
|
||||
const providers = await getProviders()
|
||||
setProviders(providers ?? undefined)
|
||||
setIsLoadingProviders(false)
|
||||
})()
|
||||
}, [status, router])
|
||||
|
||||
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setEmailValue(e.target.value)
|
||||
|
||||
const handleEmailSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setAuthLoading(true)
|
||||
const response = await signIn('email', {
|
||||
email: emailValue,
|
||||
redirect: false,
|
||||
})
|
||||
response?.error
|
||||
? showToast({
|
||||
title: 'Unauthorized',
|
||||
description: 'Sign ups are disabled.',
|
||||
})
|
||||
: showToast({
|
||||
status: 'success',
|
||||
title: 'Success!',
|
||||
description: 'Check your inbox to sign in',
|
||||
})
|
||||
setAuthLoading(false)
|
||||
}
|
||||
if (isLoadingProviders) return <Spinner />
|
||||
if (hasNoAuthProvider)
|
||||
return (
|
||||
<Text>
|
||||
You need to{' '}
|
||||
<TextLink
|
||||
href="https://docs.typebot.io/self-hosting/configuration"
|
||||
isExternal
|
||||
>
|
||||
configure at least one auth provider
|
||||
</TextLink>{' '}
|
||||
(Email, Google, GitHub, Facebook or Azure AD).
|
||||
</Text>
|
||||
)
|
||||
return (
|
||||
<Stack spacing="4" w="330px">
|
||||
<SocialLoginButtons providers={providers} />
|
||||
{providers?.email && (
|
||||
<>
|
||||
<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>
|
||||
)
|
||||
}
|
38
apps/builder/src/features/auth/components/SignInPage.tsx
Normal file
38
apps/builder/src/features/auth/components/SignInPage.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Seo } from '@/components/Seo'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { VStack, Heading, Text } from '@chakra-ui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SignInForm } from './SignInForm'
|
||||
|
||||
type Props = {
|
||||
type: 'signin' | 'signup'
|
||||
defaultEmail?: string
|
||||
}
|
||||
|
||||
export const SignInPage = ({ type }: Props) => {
|
||||
const { query } = useRouter()
|
||||
|
||||
return (
|
||||
<VStack spacing={4} h="100vh" justifyContent="center">
|
||||
<Seo title={type === 'signin' ? 'Sign In' : 'Register'} />
|
||||
<Heading
|
||||
onClick={() => {
|
||||
throw new Error('Sentry is working')
|
||||
}}
|
||||
>
|
||||
{type === 'signin' ? 'Sign In' : 'Create an account'}
|
||||
</Heading>
|
||||
{type === 'signin' ? (
|
||||
<Text>
|
||||
Don't have an account?{' '}
|
||||
<TextLink href="/register">Sign up for free</TextLink>
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
Already have an account? <TextLink href="/signin">Sign in</TextLink>
|
||||
</Text>
|
||||
)}
|
||||
<SignInForm defaultEmail={query.g?.toString()} />
|
||||
</VStack>
|
||||
)
|
||||
}
|
110
apps/builder/src/features/auth/components/SocialLoginButtons.tsx
Normal file
110
apps/builder/src/features/auth/components/SocialLoginButtons.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { Stack, Button } from '@chakra-ui/react'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import {
|
||||
ClientSafeProvider,
|
||||
LiteralUnion,
|
||||
signIn,
|
||||
useSession,
|
||||
} from 'next-auth/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { stringify } from 'qs'
|
||||
import { BuiltInProviderType } from 'next-auth/providers'
|
||||
import { GoogleLogo } from '@/components/GoogleLogo'
|
||||
import { AzureAdLogo, FacebookLogo, GitlabLogo } from './logos'
|
||||
|
||||
type Props = {
|
||||
providers:
|
||||
| Record<LiteralUnion<BuiltInProviderType, string>, ClientSafeProvider>
|
||||
| undefined
|
||||
}
|
||||
|
||||
export const SocialLoginButtons = ({ providers }: Props) => {
|
||||
const { query } = useRouter()
|
||||
const { status } = useSession()
|
||||
|
||||
const handleGitHubClick = async () =>
|
||||
signIn('github', {
|
||||
callbackUrl: `/typebots?${stringify(query)}`,
|
||||
})
|
||||
|
||||
const handleGoogleClick = async () =>
|
||||
signIn('google', {
|
||||
callbackUrl: `/typebots?${stringify(query)}`,
|
||||
})
|
||||
|
||||
const handleFacebookClick = async () =>
|
||||
signIn('facebook', {
|
||||
callbackUrl: `/typebots?${stringify(query)}`,
|
||||
})
|
||||
|
||||
const handleGitlabClick = async () =>
|
||||
signIn('gitlab', {
|
||||
callbackUrl: `/typebots?${stringify(query)}`,
|
||||
})
|
||||
|
||||
const handleAzureAdClick = async () =>
|
||||
signIn('azure-ad', {
|
||||
callbackUrl: `/typebots?${stringify(query)}`,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{providers?.github && (
|
||||
<Button
|
||||
leftIcon={<GithubIcon />}
|
||||
onClick={handleGitHubClick}
|
||||
data-testid="github"
|
||||
isLoading={['loading', 'authenticated'].includes(status)}
|
||||
variant="outline"
|
||||
>
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
)}
|
||||
{providers?.google && (
|
||||
<Button
|
||||
leftIcon={<GoogleLogo />}
|
||||
onClick={handleGoogleClick}
|
||||
data-testid="google"
|
||||
isLoading={['loading', 'authenticated'].includes(status)}
|
||||
variant="outline"
|
||||
>
|
||||
Continue with Google
|
||||
</Button>
|
||||
)}
|
||||
{providers?.facebook && (
|
||||
<Button
|
||||
leftIcon={<FacebookLogo />}
|
||||
onClick={handleFacebookClick}
|
||||
data-testid="facebook"
|
||||
isLoading={['loading', 'authenticated'].includes(status)}
|
||||
variant="outline"
|
||||
>
|
||||
Continue with Facebook
|
||||
</Button>
|
||||
)}
|
||||
{providers?.gitlab && (
|
||||
<Button
|
||||
leftIcon={<GitlabLogo />}
|
||||
onClick={handleGitlabClick}
|
||||
data-testid="gitlab"
|
||||
isLoading={['loading', 'authenticated'].includes(status)}
|
||||
variant="outline"
|
||||
>
|
||||
Continue with {providers.gitlab.name}
|
||||
</Button>
|
||||
)}
|
||||
{providers?.['azure-ad'] && (
|
||||
<Button
|
||||
leftIcon={<AzureAdLogo />}
|
||||
onClick={handleAzureAdClick}
|
||||
data-testid="azure-ad"
|
||||
isLoading={['loading', 'authenticated'].includes(status)}
|
||||
variant="outline"
|
||||
>
|
||||
Continue with {providers['azure-ad'].name}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { Icon, IconProps } from '@chakra-ui/react'
|
||||
|
||||
export const AzureAdLogo = (props: IconProps) => {
|
||||
return (
|
||||
<Icon
|
||||
id="svg1035"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 374.5 377.3"
|
||||
{...props}
|
||||
>
|
||||
<g id="layer1" transform="translate(-39.022 -78.115)">
|
||||
<g id="g1016" transform="translate(-63.947 -88.179)">
|
||||
<path
|
||||
id="path1008"
|
||||
fill='#00bef2'
|
||||
d="M290 166.3c.4 0 .8.5 1.4 1.4.5.8 42.6 51.3 93.6 112.2 51 60.9 92.6 111 92.4 111.3-.1.3-40.7 33.6-90.2 73.9s-91.6 74.6-93.5 76.2c-3.3 2.7-3.5 2.8-4.7 1.6-.7-.7-42.9-35.2-93.8-76.7S102.8 390.5 103 390c.2-.5 42-50.4 93.1-111s92.9-110.7 93.1-111.5c.2-.8.5-1.2.8-1.2z"
|
||||
/>
|
||||
<path
|
||||
id="path923"
|
||||
fill="#fff"
|
||||
stroke="#fff"
|
||||
strokeWidth="1.2357"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M283.1 483.6c-5.8-2.1-12.8-8.1-15.7-13.7-3.6-6.9-3.3-17.7.7-26.3 3.1-6.4 3.1-6.6 1.1-8.1-1.1-.8-14.4-8.2-29.4-16.3-15-8.1-28.1-15.2-29-15.7-1.2-.7-3.2 0-6.8 2.3-11.7 7.4-23.9 6.6-33.5-2.3-6.9-6.4-8.9-10.9-8.9-20.1 0-8.9 1.8-13.5 7.5-19.2 7.7-7.7 18-10.3 27.9-7 5.4 1.8 5.5 1.8 8.9-.8 4-3 36.1-32.3 51.6-47l10.7-10.2-3.2-6.7c-6.5-13.5-3.2-28.5 8.2-37.5 6.2-4.9 10.8-6.4 19.7-6.4 20.8 0 35.3 21.8 27.5 41.3-2.1 5.4-2.1 5.5-.1 8.8 1.7 2.9 30.6 37.8 45.9 55.6 2.7 3.1 5.7 5.6 6.7 5.6s4.4-1 7.6-2.2c14.9-5.9 30.6.7 36.8 15.5 4 9.5.5 22.3-8 30-6 5.4-10.4 7.1-18.4 7.1-5.6 0-7.7-.6-13.6-3.8-4.4-2.4-7.8-3.6-9.2-3.2-2.4.6-39.3 25.9-47.5 32.5-5 4.1-5.4 5.6-2.8 11.7 2.5 6 2.2 15.4-.6 21.3-3.1 6.5-10.8 13-17.5 15-6.8 1.9-10.9 1.9-16.6-.2zm1.7-110.2v-57l-3.2-4.4c-1.8-2.4-3.5-4.4-3.8-4.4-1.3 0-65.9 58.7-65.9 59.9 0 .3 1 3.3 2.2 6.5 1.2 3.3 2.1 8 2 10.7-.1 2.7-.1 5.7-.1 6.7.1 2.3 21.7 16.1 54.1 34.8 8.9 5.2 12 6.5 13.1 5.6 1.3-1.1 1.6-12.2 1.6-58.4zm27.4 50.4c42.8-26.9 50.8-32.3 51.3-34.3.3-1.2.7-5.9.8-10.6l.3-8.4-21.8-25.9c-23.4-27.7-32-37.1-34-37.1-.7 0-4.2 2-7.8 4.4l-6.6 4.4.3 56.9c.3 51 .7 59.6 2.6 59.6.2.1 7-4 14.9-9z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</Icon>
|
||||
)
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { IconProps, Icon } from '@chakra-ui/react'
|
||||
|
||||
export const FacebookLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 14222 14222" {...props}>
|
||||
<circle cx="7111" cy="7112" r="7111" fill="#1977f3" />
|
||||
<path
|
||||
d="M9879 9168l315-2056H8222V5778c0-562 275-1111 1159-1111h897V2917s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9168z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</Icon>
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
import { IconProps, Icon } from '@chakra-ui/react'
|
||||
|
||||
export const GitlabLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 256 236" {...props}>
|
||||
<path d="M128.075 236.075l47.104-144.97H80.97l47.104 144.97z" fill="#E24329" />
|
||||
<path d="M128.075 236.074L80.97 91.104H14.956l113.119 144.97z" fill="#FC6D26" />
|
||||
<path d="M14.956 91.104L.642 135.16a9.752 9.752 0 0 0 3.542 10.903l123.891 90.012-113.12-144.97z" fill="#FCA326" />
|
||||
<path d="M14.956 91.105H80.97L52.601 3.79c-1.46-4.493-7.816-4.492-9.275 0l-28.37 87.315z" fill="#E24329" />
|
||||
<path d="M128.075 236.074l47.104-144.97h66.015l-113.12 144.97z" fill="#FC6D26" />
|
||||
<path d="M241.194 91.104l14.314 44.056a9.752 9.752 0 0 1-3.543 10.903l-123.89 90.012 113.119-144.97z" fill="#FCA326" />
|
||||
<path d="M241.194 91.105h-66.015l28.37-87.315c1.46-4.493 7.816-4.492 9.275 0l28.37 87.315z" fill="#E24329" />
|
||||
</Icon>
|
||||
)
|
3
apps/builder/src/features/auth/components/logos/index.ts
Normal file
3
apps/builder/src/features/auth/components/logos/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { AzureAdLogo } from './AzureAdLogo'
|
||||
export { GitlabLogo } from './GitlabLogo'
|
||||
export { FacebookLogo } from './FacebookLogo'
|
15
apps/builder/src/features/auth/constants.ts
Normal file
15
apps/builder/src/features/auth/constants.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { User } from 'db'
|
||||
|
||||
export const mockedUser: User = {
|
||||
id: 'userId',
|
||||
name: 'John Doe',
|
||||
email: 'user@email.com',
|
||||
company: null,
|
||||
createdAt: new Date(),
|
||||
emailVerified: null,
|
||||
graphNavigation: 'TRACKPAD',
|
||||
image: 'https://avatars.githubusercontent.com/u/16015833?v=4',
|
||||
lastActivityAt: new Date(),
|
||||
onboardingCategories: [],
|
||||
updatedAt: new Date(),
|
||||
}
|
3
apps/builder/src/features/auth/index.ts
Normal file
3
apps/builder/src/features/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { SignInPage } from './components/SignInPage'
|
||||
export { getAuthenticatedUser } from './api/getAuthenticatedUser'
|
||||
export { mockedUser } from './constants'
|
Reference in New Issue
Block a user