Merge pull request #12722 from strapi/chore/events

Add events to monitor first administrator account creation
This commit is contained in:
Alexandre BODIN 2022-03-09 18:35:21 +01:00 committed by GitHub
commit a79cafc670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 234 additions and 217 deletions

View File

@ -15,7 +15,13 @@ import { Grid, GridItem } from '@strapi/design-system/Grid';
import { Typography } from '@strapi/design-system/Typography'; import { Typography } from '@strapi/design-system/Typography';
import EyeStriked from '@strapi/icons/EyeStriked'; import EyeStriked from '@strapi/icons/EyeStriked';
import Eye from '@strapi/icons/Eye'; import Eye from '@strapi/icons/Eye';
import { Form, useQuery, useNotification } from '@strapi/helper-plugin'; import {
Form,
useQuery,
useNotification,
useTracking,
getYupInnerErrors,
} from '@strapi/helper-plugin';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Formik } from 'formik'; import { Formik } from 'formik';
@ -41,12 +47,14 @@ const PasswordInput = styled(TextInput)`
} }
`; `;
const Register = ({ fieldsToDisable, noSignin, onSubmit, schema }) => { const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { push } = useHistory(); const { push } = useHistory();
const [passwordShown, setPasswordShown] = useState(false); const [passwordShown, setPasswordShown] = useState(false);
const [confirmPasswordShown, setConfirmPasswordShown] = useState(false); const [confirmPasswordShown, setConfirmPasswordShown] = useState(false);
const [submitCount, setSubmitCount] = useState(0);
const [userInfo, setUserInfo] = useState({}); const [userInfo, setUserInfo] = useState({});
const { trackUsage } = useTracking();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const query = useQuery(); const query = useQuery();
const registrationToken = query.get('registrationToken'); const registrationToken = query.get('registrationToken');
@ -97,225 +105,218 @@ const Register = ({ fieldsToDisable, noSignin, onSubmit, schema }) => {
registrationToken: registrationToken || undefined, registrationToken: registrationToken || undefined,
news: false, news: false,
}} }}
onSubmit={(data, formik) => { onSubmit={async (data, formik) => {
if (registrationToken) { try {
// We need to pass the registration token in the url param to the api in order to submit another admin user await schema.validate(data, { abortEarly: false });
onSubmit({ userInfo: omit(data, ['registrationToken']), registrationToken }, formik);
} else { if (submitCount > 0 && authType === 'register-admin') {
onSubmit(data, formik); trackUsage('didSubmitWithErrorsFirstAdmin', { count: submitCount.toString() });
}
if (registrationToken) {
// We need to pass the registration token in the url param to the api in order to submit another admin user
onSubmit(
{ userInfo: omit(data, ['registrationToken']), registrationToken },
formik
);
} else {
onSubmit(data, formik);
}
} catch (err) {
const errors = getYupInnerErrors(err);
setSubmitCount(submitCount + 1);
formik.setErrors(errors);
} }
}} }}
validationSchema={schema} // Leaving this part commented when we remove the tracking for the submitCount
// validationSchema={schema}
validateOnChange={false} validateOnChange={false}
> >
{({ values, errors, handleChange }) => ( {({ values, errors, handleChange }) => {
<Form noValidate> return (
<Main> <Form noValidate>
<Column> <Main>
<Logo /> <Column>
<Box paddingTop={6} paddingBottom={1}> <Logo />
<Typography as="h1" variant="alpha"> <Box paddingTop={6} paddingBottom={1}>
{formatMessage({ <Typography as="h1" variant="alpha">
id: 'Auth.form.welcome.title', {formatMessage({
defaultMessage: 'Welcome!', id: 'Auth.form.welcome.title',
})} defaultMessage: 'Welcome!',
</Typography>
</Box>
<CenteredBox paddingBottom={7}>
<Typography variant="epsilon" textColor="neutral600">
{formatMessage({
id: 'Auth.form.register.subtitle',
defaultMessage:
'Your credentials are only used to authenticate yourself on the admin panel. All saved data will be stored in your own database.',
})}
</Typography>
</CenteredBox>
</Column>
<Stack size={7}>
<Grid gap={4}>
<GridItem col={6}>
<TextInput
name="firstname"
required
value={values.firstname}
error={
errors.firstname
? formatMessage({
id: errors.firstname,
defaultMessage: 'This value is required.',
})
: undefined
}
onChange={handleChange}
label={formatMessage({
id: 'Auth.form.firstname.label',
defaultMessage: 'Firstname',
})} })}
/> </Typography>
</GridItem> </Box>
<GridItem col={6}> <CenteredBox paddingBottom={7}>
<TextInput <Typography variant="epsilon" textColor="neutral600">
name="lastname" {formatMessage({
value={values.lastname} id: 'Auth.form.register.subtitle',
onChange={handleChange} defaultMessage:
label={formatMessage({ 'Your credentials are only used to authenticate yourself on the admin panel. All saved data will be stored in your own database.',
id: 'Auth.form.lastname.label',
defaultMessage: 'Lastname',
})} })}
/> </Typography>
</GridItem> </CenteredBox>
</Grid> </Column>
<TextInput <Stack size={7}>
name="email" <Grid gap={4}>
disabled={fieldsToDisable.includes('email')} <GridItem col={6}>
value={values.email} <TextInput
onChange={handleChange} name="firstname"
error={ required
errors.email value={values.firstname}
? formatMessage({ error={errors.firstname ? formatMessage(errors.firstname) : undefined}
id: errors.email, onChange={handleChange}
defaultMessage: 'This value is required.', label={formatMessage({
}) id: 'Auth.form.firstname.label',
: undefined defaultMessage: 'Firstname',
} })}
required />
label={formatMessage({ </GridItem>
id: 'Auth.form.email.label', <GridItem col={6}>
defaultMessage: 'Email', <TextInput
})} name="lastname"
type="email" value={values.lastname}
/> onChange={handleChange}
<PasswordInput label={formatMessage({
name="password" id: 'Auth.form.lastname.label',
onChange={handleChange} defaultMessage: 'Lastname',
value={values.password} })}
error={ />
errors.password </GridItem>
? formatMessage({ </Grid>
id: errors.password, <TextInput
defaultMessage: 'This value is required', name="email"
}) disabled={fieldsToDisable.includes('email')}
: undefined value={values.email}
} onChange={handleChange}
endAction={ error={errors.email ? formatMessage(errors.email) : undefined}
// eslint-disable-next-line react/jsx-wrap-multilines required
<FieldActionWrapper label={formatMessage({
onClick={e => { id: 'Auth.form.email.label',
e.preventDefault(); defaultMessage: 'Email',
setPasswordShown(prev => !prev); })}
}} type="email"
label={formatMessage( />
passwordShown <PasswordInput
? { name="password"
id: 'Auth.form.password.show-password', onChange={handleChange}
defaultMessage: 'Show password', value={values.password}
} error={errors.password ? formatMessage(errors.password) : undefined}
: { endAction={
id: 'Auth.form.password.hide-password', // eslint-disable-next-line react/jsx-wrap-multilines
defaultMessage: 'Hide password', <FieldActionWrapper
} onClick={e => {
)} e.preventDefault();
> setPasswordShown(prev => !prev);
{passwordShown ? <Eye /> : <EyeStriked />} }}
</FieldActionWrapper> label={formatMessage(
} passwordShown
hint={formatMessage({ ? {
id: 'Auth.form.password.hint', id: 'Auth.form.password.show-password',
defaultMessage: defaultMessage: 'Show password',
'Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number', }
})} : {
required id: 'Auth.form.password.hide-password',
label={formatMessage({ defaultMessage: 'Hide password',
id: 'Auth.form.password.label', }
defaultMessage: 'Password', )}
})} >
type={passwordShown ? 'text' : 'password'} {passwordShown ? <Eye /> : <EyeStriked />}
/> </FieldActionWrapper>
<PasswordInput
name="confirmPassword"
onChange={handleChange}
value={values.confirmPassword}
error={
errors.confirmPassword
? formatMessage({
id: errors.confirmPassword,
defaultMessage: 'This value is required.',
})
: undefined
}
endAction={
// eslint-disable-next-line react/jsx-wrap-multilines
<FieldActionWrapper
onClick={e => {
e.preventDefault();
setConfirmPasswordShown(prev => !prev);
}}
label={formatMessage(
confirmPasswordShown
? {
id: 'Auth.form.password.show-password',
defaultMessage: 'Show password',
}
: {
id: 'Auth.form.password.hide-password',
defaultMessage: 'Hide password',
}
)}
>
{confirmPasswordShown ? <Eye /> : <EyeStriked />}
</FieldActionWrapper>
}
required
label={formatMessage({
id: 'Auth.form.confirmPassword.label',
defaultMessage: 'Confirmation Password',
})}
type={confirmPasswordShown ? 'text' : 'password'}
/>
<Checkbox
onValueChange={checked => {
handleChange({ target: { value: checked, name: 'news' } });
}}
value={values.news}
name="news"
aria-label="news"
>
{formatMessage(
{
id: 'Auth.form.register.news.label',
defaultMessage:
'Keep me updated about the new features and upcoming improvements (by doing this you accept the {terms} and the {policy}).',
},
{
terms: (
<A target="_blank" href="https://strapi.io/terms" rel="noreferrer">
{formatMessage({
id: 'Auth.privacy-policy-agreement.terms',
defaultMessage: 'terms',
})}
</A>
),
policy: (
<A target="_blank" href="https://strapi.io/privacy" rel="noreferrer">
{formatMessage({
id: 'Auth.privacy-policy-agreement.policy',
defaultMessage: 'policy',
})}
</A>
),
} }
)} hint={formatMessage({
</Checkbox> id: 'Auth.form.password.hint',
<Button fullWidth size="L" type="submit"> defaultMessage:
{formatMessage({ 'Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number',
id: 'Auth.form.button.register', })}
defaultMessage: "Let's start", required
})} label={formatMessage({
</Button> id: 'Auth.form.password.label',
</Stack> defaultMessage: 'Password',
</Main> })}
</Form> type={passwordShown ? 'text' : 'password'}
)} />
<PasswordInput
name="confirmPassword"
onChange={handleChange}
value={values.confirmPassword}
error={
errors.confirmPassword ? formatMessage(errors.confirmPassword) : undefined
}
endAction={
// eslint-disable-next-line react/jsx-wrap-multilines
<FieldActionWrapper
onClick={e => {
e.preventDefault();
setConfirmPasswordShown(prev => !prev);
}}
label={formatMessage(
confirmPasswordShown
? {
id: 'Auth.form.password.show-password',
defaultMessage: 'Show password',
}
: {
id: 'Auth.form.password.hide-password',
defaultMessage: 'Hide password',
}
)}
>
{confirmPasswordShown ? <Eye /> : <EyeStriked />}
</FieldActionWrapper>
}
required
label={formatMessage({
id: 'Auth.form.confirmPassword.label',
defaultMessage: 'Confirmation Password',
})}
type={confirmPasswordShown ? 'text' : 'password'}
/>
<Checkbox
onValueChange={checked => {
handleChange({ target: { value: checked, name: 'news' } });
}}
value={values.news}
name="news"
aria-label="news"
>
{formatMessage(
{
id: 'Auth.form.register.news.label',
defaultMessage:
'Keep me updated about the new features and upcoming improvements (by doing this you accept the {terms} and the {policy}).',
},
{
terms: (
<A target="_blank" href="https://strapi.io/terms" rel="noreferrer">
{formatMessage({
id: 'Auth.privacy-policy-agreement.terms',
defaultMessage: 'terms',
})}
</A>
),
policy: (
<A target="_blank" href="https://strapi.io/privacy" rel="noreferrer">
{formatMessage({
id: 'Auth.privacy-policy-agreement.policy',
defaultMessage: 'policy',
})}
</A>
),
}
)}
</Checkbox>
<Button fullWidth size="L" type="submit">
{formatMessage({
id: 'Auth.form.button.register',
defaultMessage: "Let's start",
})}
</Button>
</Stack>
</Main>
</Form>
);
}}
</Formik> </Formik>
{!noSignin && ( {!noSignin && (
<Box paddingTop={4}> <Box paddingTop={4}>
@ -341,10 +342,14 @@ Register.defaultProps = {
}; };
Register.propTypes = { Register.propTypes = {
authType: PropTypes.string.isRequired,
fieldsToDisable: PropTypes.array, fieldsToDisable: PropTypes.array,
noSignin: PropTypes.bool, noSignin: PropTypes.bool,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
schema: PropTypes.shape({ type: PropTypes.string.isRequired }).isRequired, schema: PropTypes.shape({
validate: PropTypes.func.isRequired,
type: PropTypes.string.isRequired,
}).isRequired,
}; };
export default Register; export default Register;

View File

@ -4,7 +4,7 @@ import camelCase from 'lodash/camelCase';
import get from 'lodash/get'; import get from 'lodash/get';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { Redirect, useRouteMatch, useHistory } from 'react-router-dom'; import { Redirect, useRouteMatch, useHistory } from 'react-router-dom';
import { auth, useQuery, useGuidedTour } from '@strapi/helper-plugin'; import { auth, useQuery, useGuidedTour, useTracking } from '@strapi/helper-plugin';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import forms from 'ee_else_ce/pages/AuthPage/utils/forms'; import forms from 'ee_else_ce/pages/AuthPage/utils/forms';
import persistStateToLocaleStorage from '../../components/GuidedTour/utils/persistStateToLocaleStorage'; import persistStateToLocaleStorage from '../../components/GuidedTour/utils/persistStateToLocaleStorage';
@ -17,6 +17,7 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
const { push } = useHistory(); const { push } = useHistory();
const { changeLocale } = useLocalesProvider(); const { changeLocale } = useLocalesProvider();
const { setSkipped } = useGuidedTour(); const { setSkipped } = useGuidedTour();
const { trackUsage } = useTracking();
const { const {
params: { authType }, params: { authType },
} = useRouteMatch('/auth/:authType'); } = useRouteMatch('/auth/:authType');
@ -146,6 +147,8 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
const registerRequest = async (body, requestURL, { setSubmitting, setErrors }) => { const registerRequest = async (body, requestURL, { setSubmitting, setErrors }) => {
try { try {
trackUsage('willCreateFirstAdmin');
const { const {
data: { data: {
data: { token, user }, data: { token, user },
@ -189,6 +192,8 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
// Redirect to the homePage // Redirect to the homePage
push('/'); push('/');
} catch (err) { } catch (err) {
trackUsage('didNotCreateFirstAdmin');
if (err.response) { if (err.response) {
const { data } = err.response; const { data } = err.response;
const apiErrors = formatAPIErrors(data); const apiErrors = formatAPIErrors(data);
@ -249,6 +254,7 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
return ( return (
<Component <Component
{...rest} {...rest}
authType={authType}
fieldsToDisable={fieldsToDisable} fieldsToDisable={fieldsToDisable}
formErrors={formErrors} formErrors={formErrors}
inputsPrefix={inputsPrefix} inputsPrefix={inputsPrefix}

View File

@ -205,7 +205,13 @@ class Strapi {
this.config.get('admin.autoOpen', true) !== false; this.config.get('admin.autoOpen', true) !== false;
if (shouldOpenAdmin && !isInitialized) { if (shouldOpenAdmin && !isInitialized) {
await utils.openBrowser(this.config); try {
await utils.openBrowser(this.config);
this.telemetry.send('didOpenTab');
} catch (e) {
this.telemetry.send('didNotOpenTab');
}
;
} }
} }