Flowise/packages/ui/src/views/auth/register.jsx

473 lines
21 KiB
JavaScript

import { useEffect, useState } from 'react'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { z } from 'zod'
// material-ui
import { Alert, Box, Button, Divider, Icon, List, ListItemText, OutlinedInput, Stack, Typography, useTheme } from '@mui/material'
// project imports
import { StyledButton } from '@/ui-component/button/StyledButton'
import { Input } from '@/ui-component/input/Input'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
// API
import accountApi from '@/api/account.api'
import loginMethodApi from '@/api/loginmethod'
import ssoApi from '@/api/sso'
// Hooks
import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
// utils
import useNotifier from '@/utils/useNotifier'
import { passwordSchema } from '@/utils/validation'
// Icons
import Auth0SSOLoginIcon from '@/assets/images/auth0.svg'
import GithubSSOLoginIcon from '@/assets/images/github.svg'
import GoogleSSOLoginIcon from '@/assets/images/google.svg'
import AzureSSOLoginIcon from '@/assets/images/microsoft-azure.svg'
import { store } from '@/store'
import { loginSuccess } from '@/store/reducers/authSlice'
import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react'
// ==============================|| Register ||============================== //
// IMPORTANT: when updating this schema, update the schema on the server as well
// packages/server/src/enterprise/Interface.Enterprise.ts
const RegisterEnterpriseUserSchema = z
.object({
username: z.string().min(1, 'Name is required'),
email: z.string().min(1, 'Email is required').email('Invalid email address'),
password: passwordSchema,
confirmPassword: z.string().min(1, 'Confirm Password is required'),
token: z.string().min(1, 'Invite Code is required')
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword']
})
const RegisterCloudUserSchema = z
.object({
username: z.string().min(1, 'Name is required'),
email: z.string().min(1, 'Email is required').email('Invalid email address'),
password: passwordSchema,
confirmPassword: z.string().min(1, 'Confirm Password is required')
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword']
})
const RegisterPage = () => {
const theme = useTheme()
useNotifier()
const { isEnterpriseLicensed, isCloud, isOpenSource } = useConfig()
const usernameInput = {
label: 'Username',
name: 'username',
type: 'text',
placeholder: 'John Doe'
}
const passwordInput = {
label: 'Password',
name: 'password',
type: 'password',
placeholder: '********'
}
const confirmPasswordInput = {
label: 'Confirm Password',
name: 'confirmPassword',
type: 'password',
placeholder: '********'
}
const emailInput = {
label: 'EMail',
name: 'email',
type: 'email',
placeholder: 'user@company.com'
}
const inviteCodeInput = {
label: 'Invite Code',
name: 'inviteCode',
type: 'text'
}
const [params] = useSearchParams()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [token, setToken] = useState(params.get('token') ?? '')
const [username, setUsername] = useState('')
const [configuredSsoProviders, setConfiguredSsoProviders] = useState([])
const [loading, setLoading] = useState(false)
const [authError, setAuthError] = useState('')
const [successMsg, setSuccessMsg] = useState(undefined)
const registerApi = useApi(accountApi.registerAccount)
const ssoLoginApi = useApi(ssoApi.ssoLogin)
const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods)
const navigate = useNavigate()
const register = async (event) => {
event.preventDefault()
if (isEnterpriseLicensed) {
const result = RegisterEnterpriseUserSchema.safeParse({
username,
email,
token,
password,
confirmPassword
})
if (result.success) {
setLoading(true)
const body = {
user: {
name: username,
email,
credential: password,
tempToken: token
}
}
await registerApi.request(body)
} else {
const errorMessages = result.error.errors.map((err) => err.message)
setAuthError(errorMessages.join(', '))
}
} else if (isCloud) {
const formData = new FormData(event.target)
const referral = formData.get('referral')
const result = RegisterCloudUserSchema.safeParse({
username,
email,
password,
confirmPassword
})
if (result.success) {
setLoading(true)
const body = {
user: {
name: username,
email,
credential: password
}
}
if (referral) {
body.user.referral = referral
}
await registerApi.request(body)
} else {
const errorMessages = result.error.errors.map((err) => err.message)
setAuthError(errorMessages.join(', '))
}
}
}
const signInWithSSO = (ssoProvider) => {
//ssoLoginApi.request(ssoProvider)
window.location.href = `/api/v1/${ssoProvider}/login`
}
useEffect(() => {
if (registerApi.error) {
if (isEnterpriseLicensed) {
setAuthError(
`Error in registering user. Please contact your administrator. (${registerApi.error?.response?.data?.message})`
)
} else if (isCloud) {
setAuthError(`Error in registering user. Please try again.`)
}
setLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [registerApi.error])
useEffect(() => {
if (!isOpenSource) {
getDefaultProvidersApi.request()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (ssoLoginApi.data) {
store.dispatch(loginSuccess(ssoLoginApi.data))
navigate(location.state?.path || '/')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ssoLoginApi.data])
useEffect(() => {
if (ssoLoginApi.error) {
if (ssoLoginApi.error?.response?.status === 401 && ssoLoginApi.error?.response?.data.redirectUrl) {
window.location.href = ssoLoginApi.error.response.data.redirectUrl
} else {
setAuthError(ssoLoginApi.error.message)
}
}
}, [ssoLoginApi.error])
useEffect(() => {
if (getDefaultProvidersApi.data && getDefaultProvidersApi.data.providers) {
//data is an array of objects, store only the provider attribute
setConfiguredSsoProviders(getDefaultProvidersApi.data.providers.map((provider) => provider))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getDefaultProvidersApi.data])
useEffect(() => {
if (registerApi.data) {
setLoading(false)
setAuthError(undefined)
setConfirmPassword('')
setPassword('')
setToken('')
setUsername('')
setEmail('')
if (isEnterpriseLicensed) {
setSuccessMsg('Registration Successful. You will be redirected to the sign in page shortly.')
} else if (isCloud) {
setSuccessMsg('To complete your registration, please click on the verification link we sent to your email address')
}
setTimeout(() => {
navigate('/signin')
}, 3000)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [registerApi.data])
return (
<>
<Box
sx={{
width: '100%',
maxHeight: '100vh',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '24px'
}}
>
<Stack flexDirection='column' sx={{ width: '480px', gap: 3 }}>
{authError && (
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
{authError.split(', ').length > 0 ? (
<List dense sx={{ py: 0 }}>
{authError.split(', ').map((error, index) => (
<ListItemText key={index} primary={error} primaryTypographyProps={{ color: '#fff !important' }} />
))}
</List>
) : (
authError
)}
</Alert>
)}
{successMsg && (
<Alert icon={<IconCircleCheck />} variant='filled' severity='success'>
{successMsg}
</Alert>
)}
<Stack sx={{ gap: 1 }}>
<Typography variant='h1'>Sign Up</Typography>
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>
Already have an account?{' '}
<Link style={{ color: theme.palette.primary.main }} to='/signin'>
Sign In
</Link>
.
</Typography>
</Stack>
<form onSubmit={register} data-rewardful>
<Stack sx={{ width: '100%', flexDirection: 'column', alignItems: 'left', justifyContent: 'center', gap: 2 }}>
<Box>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Full Name<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<Input
inputParam={usernameInput}
placeholder='Display Name'
onChange={(newValue) => setUsername(newValue)}
value={username}
showDialog={false}
/>
<Typography variant='caption'>
<i>Is used for display purposes only.</i>
</Typography>
</Box>
<Box>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Email<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<Input
inputParam={emailInput}
onChange={(newValue) => setEmail(newValue)}
value={email}
showDialog={false}
/>
<Typography variant='caption'>
<i>Kindly use a valid email address. Will be used as login id.</i>
</Typography>
</Box>
{isEnterpriseLicensed && (
<Box>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Invite Code<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
fullWidth
type='string'
placeholder='Paste in the invite code.'
multiline={false}
inputParam={inviteCodeInput}
onChange={(e) => setToken(e.target.value)}
value={token}
/>
<Typography variant='caption'>
<i>Please copy the token you would have received in your email.</i>
</Typography>
</Box>
)}
<Box>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Password<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<Input inputParam={passwordInput} onChange={(newValue) => setPassword(newValue)} value={password} />
<Typography variant='caption'>
<i>
Password must be at least 8 characters long and contain at least one lowercase letter, one uppercase
letter, one digit, and one special character.
</i>
</Typography>
</Box>
<Box>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Confirm Password<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<Input
inputParam={confirmPasswordInput}
onChange={(newValue) => setConfirmPassword(newValue)}
value={confirmPassword}
/>
<Typography variant='caption'>
<i>Confirm your password. Must match the password typed above.</i>
</Typography>
</Box>
<StyledButton variant='contained' style={{ borderRadius: 12, height: 40, marginRight: 5 }} type='submit'>
Create Account
</StyledButton>
{configuredSsoProviders.length > 0 && <Divider sx={{ width: '100%' }}>OR</Divider>}
{configuredSsoProviders &&
configuredSsoProviders.map(
(ssoProvider) =>
//https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps
ssoProvider === 'azure' && (
<Button
key={ssoProvider}
variant='outlined'
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
onClick={() => signInWithSSO(ssoProvider)}
startIcon={
<Icon>
<img src={AzureSSOLoginIcon} alt={'MicrosoftSSO'} width={20} height={20} />
</Icon>
}
>
Sign In With Microsoft
</Button>
)
)}
{configuredSsoProviders &&
configuredSsoProviders.map(
(ssoProvider) =>
ssoProvider === 'google' && (
<Button
key={ssoProvider}
variant='outlined'
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
onClick={() => signInWithSSO(ssoProvider)}
startIcon={
<Icon>
<img src={GoogleSSOLoginIcon} alt={'GoogleSSO'} width={20} height={20} />
</Icon>
}
>
Sign In With Google
</Button>
)
)}
{configuredSsoProviders &&
configuredSsoProviders.map(
(ssoProvider) =>
ssoProvider === 'auth0' && (
<Button
key={ssoProvider}
variant='outlined'
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
onClick={() => signInWithSSO(ssoProvider)}
startIcon={
<Icon>
<img src={Auth0SSOLoginIcon} alt={'Auth0SSO'} width={20} height={20} />
</Icon>
}
>
Sign In With Auth0 by Okta
</Button>
)
)}
{configuredSsoProviders &&
configuredSsoProviders.map(
(ssoProvider) =>
ssoProvider === 'github' && (
<Button
key={ssoProvider}
variant='outlined'
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
onClick={() => signInWithSSO(ssoProvider)}
startIcon={
<Icon>
<img src={GithubSSOLoginIcon} alt={'GithubSSO'} width={20} height={20} />
</Icon>
}
>
Sign In With Github
</Button>
)
)}
</Stack>
</form>
</Stack>
</Box>
{loading && <BackdropLoader open={loading} />}
</>
)
}
export default RegisterPage