1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Create Signup page for users from Invite link (#2052)

* refactor: user creation screen cleanup

* feat: deprecation notice for google sso

* fix: docs openid typo

* user invite hook mock
This commit is contained in:
Tymoteusz Czech 2022-09-14 11:42:20 +02:00 committed by GitHub
parent 51c7ea053e
commit ce3db75133
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 256 additions and 205 deletions

View File

@ -1,5 +1,6 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import {
Box,
Button, Button,
FormControlLabel, FormControlLabel,
Grid, Grid,
@ -75,23 +76,26 @@ export const GoogleAuth = () => {
return ( return (
<PageContent> <PageContent>
<Grid container style={{ marginBottom: '1rem' }}> <Box>
<Grid item xs={12}> <Alert severity="error" sx={{ mb: 2 }}>
<Alert severity="info"> This integration is deprecated and will be removed in next
Please read the{' '} major version. Please use <strong>OpenID Connect</strong> to
<a enable Google SSO.
href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" </Alert>
target="_blank" <Alert severity="info" sx={{ mb: 3 }}>
rel="noreferrer" Read the{' '}
> <a
documentation href="https://www.unleash-hosted.com/docs/enterprise-authentication/google"
</a>{' '} target="_blank"
to learn how to integrate with Google OAuth 2.0. <br /> rel="noreferrer"
Callback URL:{' '} >
<code>{uiConfig.unleashUrl}/auth/google/callback</code> documentation
</Alert> </a>{' '}
</Grid> to learn how to integrate with Google OAuth 2.0. <br />
</Grid> Callback URL:{' '}
<code>{uiConfig.unleashUrl}/auth/google/callback</code>
</Alert>
</Box>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Grid container spacing={3} mb={2}> <Grid container spacing={3} mb={2}>
<Grid item xs={5}> <Grid item xs={5}>

View File

@ -1,49 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
newUser: {
width: '350px',
[theme.breakpoints.down('sm')]: {
width: '100%',
},
},
title: {
fontSize: theme.fontSizes.mainHeader,
marginBottom: '1.25rem',
textAlign: 'center',
},
inviteText: {
marginBottom: '1rem',
textAlign: 'center',
},
container: {
display: 'flex',
},
roleContainer: {
marginTop: '2rem',
},
innerContainer: {
width: '60%',
padding: '4rem 3rem',
},
buttonContainer: {
display: 'flex',
marginTop: '1rem',
},
primaryBtn: {
marginRight: '8px',
},
subtitle: {
margin: '0.5rem 0',
},
passwordHeader: {
marginTop: '2rem',
},
emailField: {
minWidth: '300px',
width: '100%',
[theme.breakpoints.down('sm')]: {
minWidth: '100%',
},
},
}));

View File

@ -1,113 +1,137 @@
import useLoading from 'hooks/useLoading'; import { Box, TextField, Typography } from '@mui/material';
import { TextField, Typography } from '@mui/material';
import StandaloneBanner from '../StandaloneBanner/StandaloneBanner';
import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
import { useStyles } from './NewUser.styles';
import useResetPassword from 'hooks/api/getters/useResetPassword/useResetPassword'; import useResetPassword from 'hooks/api/getters/useResetPassword/useResetPassword';
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import InvalidToken from '../common/InvalidToken/InvalidToken';
import AuthOptions from '../common/AuthOptions/AuthOptions'; import AuthOptions from '../common/AuthOptions/AuthOptions';
import DividerText from 'component/common/DividerText/DividerText'; import DividerText from 'component/common/DividerText/DividerText';
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails'; import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
import { useInviteUserToken } from 'hooks/api/getters/useInviteUserToken/useInviteUserToken';
import ResetPasswordForm from '../common/ResetPasswordForm/ResetPasswordForm';
import InvalidToken from '../common/InvalidToken/InvalidToken';
import { NewUserWrapper } from './NewUserWrapper/NewUserWrapper';
export const NewUser = () => { export const NewUser = () => {
const { authDetails } = useAuthDetails(); const { authDetails } = useAuthDetails();
const { token, data, loading, setLoading, invalidToken } = const {
useResetPassword(); token,
const ref = useLoading(loading); data,
const { classes: styles } = useStyles(); loading: resetLoading,
setLoading,
invalidToken,
} = useResetPassword();
const { invite, loading: inviteLoading } = useInviteUserToken();
const passwordDisabled = authDetails?.defaultHidden === true;
if (invalidToken && !invite) {
return (
<NewUserWrapper loading={resetLoading || inviteLoading}>
<InvalidToken />
</NewUserWrapper>
);
}
return ( return (
<div ref={ref}> <NewUserWrapper
<StandaloneLayout loading={resetLoading || inviteLoading}
showMenu={false} title={
BannerComponent={<StandaloneBanner title={'Unleash'} />} passwordDisabled
> ? 'Connect your account and start your journey'
<div className={styles.newUser}> : 'Enter your personal details and start your journey'
<ConditionallyRender }
condition={invalidToken} >
show={<InvalidToken />} <ConditionallyRender
elseShow={ condition={data?.createdBy}
<ResetPasswordDetails show={
token={token} <Typography
setLoading={setLoading} variant="body1"
> data-loading
<h2 className={styles.title}> sx={{ textAlign: 'center', mb: 2 }}
Enter your personal details and start your >
journey {data?.createdBy}
</h2> <br /> has invited you to join Unleash.
<ConditionallyRender </Typography>
condition={data?.createdBy} }
show={ />
<Typography <Typography color="text.secondary">
variant="body1" We suggest using{' '}
data-loading <Typography component="strong" fontWeight="bold">
className={styles.inviteText} the email you use for work
> </Typography>
{data?.createdBy} .
<br></br> has invited you to join </Typography>
Unleash. <ConditionallyRender
</Typography> condition={Boolean(authDetails?.options?.length)}
} show={
/> <Box sx={{ mt: 2 }}>
<AuthOptions options={authDetails?.options} />
</Box>
}
/>
<ConditionallyRender
condition={
Boolean(authDetails?.options?.length) && !passwordDisabled
}
show={
<DividerText
text="or sign-up with an email address"
data-loading
/>
}
/>
<ConditionallyRender
condition={!passwordDisabled}
show={
<>
<ConditionallyRender
condition={data?.email}
show={() => (
<Typography <Typography
data-loading data-loading
variant="body1" variant="body1"
className={styles.subtitle} sx={{ my: 1 }}
> >
Your username is Your username is
</Typography> </Typography>
)}
/>
<TextField
data-loading
type="email"
value={data?.email || ''}
id="username"
label="Email"
variant="outlined"
size="small"
sx={{ my: 1 }}
disabled={Boolean(data?.email)}
fullWidth
required
/>
<ConditionallyRender
condition={Boolean(invite)}
show={() => (
<TextField <TextField
data-loading data-loading
value={data?.email || ''} value=""
id="username" id="username"
label="Username" label="Full name"
variant="outlined" variant="outlined"
size="small" size="small"
className={styles.emailField} sx={{ my: 1 }}
disabled fullWidth
required
/> />
<div className={styles.roleContainer}> )}
<ConditionallyRender />
condition={Boolean( <Typography variant="body1" data-loading sx={{ mt: 2 }}>
authDetails?.options?.length Set a password for your account.
)} </Typography>
show={ <ResetPasswordForm
<> token={token}
<DividerText setLoading={setLoading}
text="sign in with" />
data-loading </>
/> }
/>
<AuthOptions </NewUserWrapper>
options={
authDetails?.options
}
/>
<DividerText
text="or set a new password for your account"
data-loading
/>
</>
}
elseShow={
<Typography
variant="body1"
data-loading
>
Set a password for your account.
</Typography>
}
/>
</div>
</ResetPasswordDetails>
}
/>
</div>
</StandaloneLayout>
</div>
); );
}; };

View File

@ -0,0 +1,53 @@
import { FC } from 'react';
import { Box, Typography } from '@mui/material';
import StandaloneLayout from 'component/user/common/StandaloneLayout/StandaloneLayout';
import StandaloneBanner from 'component/user/StandaloneBanner/StandaloneBanner';
import useLoading from 'hooks/useLoading';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface INewUserWrapperProps {
loading?: boolean;
title?: string;
}
export const NewUserWrapper: FC<INewUserWrapperProps> = ({
children,
loading,
title,
}) => {
const ref = useLoading(loading || false);
return (
<div ref={ref}>
<StandaloneLayout
showMenu={false}
BannerComponent={<StandaloneBanner title={'Unleash'} />}
>
<Box
sx={{
width: ['100%', '350px'],
}}
>
<ConditionallyRender
condition={Boolean(title)}
show={
<Typography
component="h2"
sx={{
fontSize: theme =>
theme.fontSizes.mainHeader,
marginBottom: 2,
textAlign: 'center',
fontWeight: theme => theme.fontWeight.bold,
}}
>
{title}
</Typography>
}
/>
{children}
</Box>
</StandaloneLayout>
</div>
);
};

View File

@ -1,13 +1,11 @@
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
import { useStyles } from './ResetPassword.styles'; import { useStyles } from './ResetPassword.styles';
import { Typography } from '@mui/material'; import { Typography } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import InvalidToken from '../common/InvalidToken/InvalidToken'; import InvalidToken from '../common/InvalidToken/InvalidToken';
import useResetPassword from 'hooks/api/getters/useResetPassword/useResetPassword'; import useResetPassword from 'hooks/api/getters/useResetPassword/useResetPassword';
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout'; import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
import ResetPasswordForm from '../common/ResetPasswordForm/ResetPasswordForm';
const ResetPassword = () => { const ResetPassword = () => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
@ -22,10 +20,7 @@ const ResetPassword = () => {
condition={invalidToken} condition={invalidToken}
show={<InvalidToken />} show={<InvalidToken />}
elseShow={ elseShow={
<ResetPasswordDetails <>
token={token}
setLoading={setLoading}
>
<Typography <Typography
variant="h2" variant="h2"
className={styles.title} className={styles.title}
@ -33,7 +28,12 @@ const ResetPassword = () => {
> >
Reset password Reset password
</Typography> </Typography>
</ResetPasswordDetails>
<ResetPasswordForm
token={token}
setLoading={setLoading}
/>
</>
} }
/> />
</div> </div>

View File

@ -1,11 +1,17 @@
import { VFC } from 'react';
import { Button, Typography } from '@mui/material'; import { Button, Typography } from '@mui/material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { INVALID_TOKEN_BUTTON } from 'utils/testIds'; import { INVALID_TOKEN_BUTTON } from 'utils/testIds';
import { useThemeStyles } from 'themes/themeStyles'; import { useThemeStyles } from 'themes/themeStyles';
import classnames from 'classnames'; import classnames from 'classnames';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
const InvalidToken = () => { const InvalidToken: VFC = () => {
const { authDetails } = useAuthDetails();
const { classes: themeStyles } = useThemeStyles(); const { classes: themeStyles } = useThemeStyles();
const passwordDisabled = authDetails?.defaultHidden === true;
return ( return (
<div <div
className={classnames( className={classnames(
@ -17,20 +23,44 @@ const InvalidToken = () => {
<Typography variant="h2" className={themeStyles.title}> <Typography variant="h2" className={themeStyles.title}>
Invalid token Invalid token
</Typography> </Typography>
<Typography variant="subtitle1"> <ConditionallyRender
Your token has either been used to reset your password, or it condition={passwordDisabled}
has expired. Please request a new reset password URL in order to show={
reset your password. <>
</Typography> <Typography variant="subtitle1">
<Button Your instance does not support password
variant="contained" authentication. Use correct work email to access
color="primary" your account.
component={Link} </Typography>
to="forgotten-password" <Button
data-testid={INVALID_TOKEN_BUTTON} variant="contained"
> color="primary"
Reset password component={Link}
</Button> to="/login"
>
Login
</Button>
</>
}
elseShow={
<>
<Typography variant="subtitle1">
Your token has either been used to reset your
password, or it has expired. Please request a new
reset password URL in order to reset your password.
</Typography>
<Button
variant="contained"
color="primary"
component={Link}
to="/forgotten-password"
data-testid={INVALID_TOKEN_BUTTON}
>
Reset password
</Button>
</>
}
/>
</div> </div>
); );
}; };

View File

@ -1,22 +0,0 @@
import { FC, Dispatch, SetStateAction } from 'react';
import ResetPasswordForm from '../ResetPasswordForm/ResetPasswordForm';
interface IResetPasswordDetails {
token: string;
setLoading: Dispatch<SetStateAction<boolean>>;
}
const ResetPasswordDetails: FC<IResetPasswordDetails> = ({
children,
token,
setLoading,
}) => {
return (
<div style={{ width: '100%' }}>
{children}
<ResetPasswordForm token={token} setLoading={setLoading} />
</div>
);
};
export default ResetPasswordDetails;

View File

@ -3,6 +3,7 @@ import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({ export const useStyles = makeStyles()(theme => ({
matcherContainer: { matcherContainer: {
position: 'relative', position: 'relative',
paddingTop: theme.spacing(0.5),
}, },
matcherIcon: { matcherIcon: {
marginRight: '5px', marginRight: '5px',

View File

@ -74,7 +74,7 @@ const ResetPasswordForm = ({ token, setLoading }: IResetPasswordProps) => {
const res = await makeResetPasswordReq(); const res = await makeResetPasswordReq();
setLoading(false); setLoading(false);
if (res.status === OK) { if (res.status === OK) {
navigate('login?reset=true'); navigate('/login?reset=true');
setApiError(false); setApiError(false);
} else { } else {
setApiError(true); setApiError(true);

View File

@ -0,0 +1,10 @@
import useQueryParams from 'hooks/useQueryParams';
export const useInviteUserToken = () => {
const query = useQueryParams();
const invite = query.get('invite') || '';
// TODO: Invite token API
return { invite, loading: false };
};

View File

@ -1,9 +1,9 @@
--- ---
id: sso-google id: sso-google
title: "[Deprecated] How to add SSO with Google" title: '[Deprecated] How to add SSO with Google'
--- ---
> Single Sign-on via the Google Authenticator provider is deprecated. We recommend using [OpenId Connect](./sso-open-id-connect.md) instead. > Single Sign-on via the Google Authenticator provider is deprecated. We recommend using [OpenID Connect](./sso-open-id-connect.md) instead.
## Introduction {#introduction} ## Introduction {#introduction}

View File

@ -1,6 +1,6 @@
--- ---
id: sso-open-id-connect id: sso-open-id-connect
title: How to add SSO with OpenId Connect title: How to add SSO with OpenID Connect
--- ---
> The **Single-Sign-On capability** is only available for customers on the Enterprise subscription. Check out the [Unleash plans](https://www.getunleash.io/plans) for details. > The **Single-Sign-On capability** is only available for customers on the Enterprise subscription. Check out the [Unleash plans](https://www.getunleash.io/plans) for details.

View File

@ -5,7 +5,7 @@ title: Securing Unleash
**If you are still using Unleash v3 you need to follow the [securing-unleash-v3](./securing-unleash-v3)** **If you are still using Unleash v3 you need to follow the [securing-unleash-v3](./securing-unleash-v3)**
> This guide is only relevant if you are using Unleash Open-Source. The Enterprise edition does already ship with multiple SSO options, such as SAML 2.0, OpenId Connect. > This guide is only relevant if you are using Unleash Open-Source. The Enterprise edition does already ship with multiple SSO options, such as SAML 2.0, OpenID Connect.
Unleash Open-Source v4 comes with username/password authentication out of the box. In addition Unleash v4 also comes with API token support, to make it easy to handle access tokens for Client SDKs and programmatic access to the Unleash APIs. Unleash Open-Source v4 comes with username/password authentication out of the box. In addition Unleash v4 also comes with API token support, to make it easy to handle access tokens for Client SDKs and programmatic access to the Unleash APIs.