@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
258 lines (256 loc) • 9.63 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from 'react';
import Typography from '@mui/material/Typography';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import { ListItem } from '@mui/material';
import ListItemText from '@mui/material/ListItemText';
import Divider from '@mui/material/Divider';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import CheckCircleOutlineRoundedIcon from '@mui/icons-material/CheckCircleOutlineRounded';
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
import Collapse from '@mui/material/Collapse';
import { isBlank } from '../../utils/string';
import { nou } from '../../utils/object';
function getStyles(sx) {
return {
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '330px'
},
scoreList: Object.assign(
{
display: 'flex',
flexDirection: 'row',
padding: 0,
columnGap: '5px',
marginTop: (theme) => theme.spacing(1),
marginBottom: '5px'
},
sx === null || sx === void 0 ? void 0 : sx.scoreList
),
scoreListItem: Object.assign(
{ width: 'auto', padding: 0 },
sx === null || sx === void 0 ? void 0 : sx.scoreListItem
),
scoreListItemDisplay: Object.assign(
{ width: '40px', textAlign: 'center', borderTop: '4px solid', borderColor: (theme) => theme.palette.grey.A400 },
sx === null || sx === void 0 ? void 0 : sx.scoreListItemDisplay
),
scoreListItemText: Object.assign({ margin: 0 }, sx === null || sx === void 0 ? void 0 : sx.scoreListItemText),
scoreActive20: Object.assign(
{ borderColor: (theme) => theme.palette.error.light },
sx === null || sx === void 0 ? void 0 : sx.scoreActive20
),
scoreActive40: Object.assign({ borderColor: '#FB8C00' }, sx === null || sx === void 0 ? void 0 : sx.scoreActive40),
scoreActive60: Object.assign({ borderColor: '#FFB400' }, sx === null || sx === void 0 ? void 0 : sx.scoreActive60),
scoreActive80: Object.assign(
{ borderColor: (theme) => theme.palette.info.light },
sx === null || sx === void 0 ? void 0 : sx.scoreActive80
),
scoreActive100: Object.assign(
{ borderColor: (theme) => theme.palette.success.light },
sx === null || sx === void 0 ? void 0 : sx.scoreActive100
),
yourScoreText: Object.assign(
{ display: 'flex', alignItems: 'center' },
sx === null || sx === void 0 ? void 0 : sx.yourScoreText
),
yourScoreTextInvalid: Object.assign(
{ color: (theme) => theme.palette.error.main },
sx === null || sx === void 0 ? void 0 : sx.yourScoreTextInvalid
),
yourScoreTextValid: Object.assign(
{ color: (theme) => theme.palette.success.main },
sx === null || sx === void 0 ? void 0 : sx.yourScoreTextValid
),
yourScoreIcon: Object.assign(
{ fontSize: '15px', marginRight: (theme) => theme.spacing(1) },
sx === null || sx === void 0 ? void 0 : sx.yourScoreIcon
),
feedbackContainer: Object.assign(
{ width: '100%', alignItems: 'center' },
sx === null || sx === void 0 ? void 0 : sx.feedbackContainer
),
divider: Object.assign(
{ width: '40%', margin: (theme) => `${theme.spacing(1)} auto` },
sx === null || sx === void 0 ? void 0 : sx.divider
)
};
}
const messages = defineMessages({
0: {
id: 'passwordStrengthDisplay.tooGuessable',
defaultMessage: 'Too guessable'
},
1: {
id: 'passwordStrengthDisplay.veryGuessable',
defaultMessage: 'Very guessable'
},
2: {
id: 'passwordStrengthDisplay.somewhatGuessable',
defaultMessage: 'Somewhat guessable'
},
3: {
id: 'passwordStrengthDisplay.safelyUnguessable',
defaultMessage: 'Safely unguessable'
},
4: {
id: 'passwordStrengthDisplay.veryUnguessable',
defaultMessage: 'Very unguessable'
}
});
function getDisplayScore(score) {
return (score + 1) * 20;
}
export function PasswordStrengthDisplay(props) {
const { value, passwordRequirementsMinComplexity, onValidStateChanged, sxs } = props;
const sx = getStyles(sxs);
const minScore = getDisplayScore(passwordRequirementsMinComplexity);
const [password, setPassword] = useState(null);
const passwordScore = value === '' || nou(password) ? 0 : getDisplayScore(password.score);
const { formatMessage } = useIntl();
const onChangeTimeoutRef = useRef(null);
useEffect(() => {
clearTimeout(onChangeTimeoutRef.current);
onChangeTimeoutRef.current = setTimeout(() => {
import('zxcvbn').then(({ default: zxcvbn }) => {
const pass = zxcvbn(value);
setPassword(pass);
onValidStateChanged(isBlank(value) ? null : pass.score >= passwordRequirementsMinComplexity);
});
}, 200);
}, [value, onValidStateChanged, passwordRequirementsMinComplexity]);
return React.createElement(
Box,
{ sx: sx.container },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, {
id: 'passwordStrengthDisplay.passwordStrengthTitle',
defaultMessage: 'Password Strength Score'
})
),
React.createElement(
Typography,
{ variant: 'body2' },
React.createElement(FormattedMessage, {
id: 'passwordStrengthDisplay.minimumScore',
defaultMessage: 'Minimum score {minScore}',
values: { minScore }
})
),
React.createElement(
Box,
null,
React.createElement(
List,
{ sx: sx.scoreList },
new Array(5)
.fill(null)
.map((x, i) =>
React.createElement(
ListItem,
{ sx: sx.scoreListItem, key: i },
React.createElement(
Box,
{
sx: Object.assign(
Object.assign({}, sx.scoreListItemDisplay),
passwordScore >= getDisplayScore(i) && Object.assign({}, sx[`scoreActive${getDisplayScore(i)}`])
)
},
React.createElement(ListItemText, { primary: getDisplayScore(i), sx: sx.scoreListItemText })
)
)
)
)
),
React.createElement(
Typography,
{
variant: 'body2',
/* @ts-ignore - spread styles not recognized as valid */
sx: Object.assign(
Object.assign({}, sx.yourScoreText),
passwordScore > 0 && passwordScore < minScore
? Object.assign({}, sx.yourScoreTextInvalid)
: passwordScore >= minScore
? Object.assign({}, sx.yourScoreTextValid)
: {}
)
},
passwordScore === minScore
? React.createElement(CheckCircleOutlineRoundedIcon, { sx: sx.yourScoreIcon })
: passwordScore === 100
? React.createElement(StarOutlineRoundedIcon, { sx: sx.yourScoreIcon })
: React.createElement(InfoOutlinedIcon, { sx: sx.yourScoreIcon }),
React.createElement(FormattedMessage, {
id: 'passwordStrengthDisplay.passwordScore',
defaultMessage: 'Your score: {score}',
values: { score: passwordScore }
})
),
React.createElement(
Collapse,
{ in: passwordScore > 0 && passwordScore < minScore, sx: sx.feedbackContainer },
React.createElement(Divider, { sx: sx.divider }),
React.createElement(
Box,
{ style: { textAlign: 'left' } },
password &&
React.createElement(
React.Fragment,
null,
React.createElement(
Typography,
{ variant: 'body2' },
formatMessage(messages[password.score]),
'.',
' ',
password.feedback.warning ? `${password.feedback.warning}.` : ''
),
password === null || password === void 0
? void 0
: password.feedback.suggestions.map((suggestion, i) =>
React.createElement(Typography, { variant: 'body2', key: i }, '- ', suggestion)
)
)
)
)
);
}
export default PasswordStrengthDisplay;