diff --git a/src/Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.jsx b/src/Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.jsx new file mode 100644 index 00000000..c1800bcf --- /dev/null +++ b/src/Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.jsx @@ -0,0 +1,42 @@ +import React, { useEffect } from "react"; + +import { bytesLength } from "../../../utils/index.js"; + +import "./PasswordComplexityIndicator.scss"; + +const PasswordComplexityIndicator = ({ + password, + complexity, + setComplexity, +}) => { + useEffect(() => { + const passwordLength = bytesLength(password); + + const length = passwordLength >= 8 && passwordLength <= 72; + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasNumbers = /\d/.test(password); + const hasNonAlphas = /\W/.test(password); + + if (length) { + setComplexity( + length + hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphas + ); + } else if (passwordLength !== 0) { + setComplexity(1); + } else { + setComplexity(0); + } + }, [password, setComplexity]); + + return ( +
+
+
+ ); +}; + +export default PasswordComplexityIndicator; diff --git a/src/Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.scss b/src/Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.scss new file mode 100644 index 00000000..6b792721 --- /dev/null +++ b/src/Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.scss @@ -0,0 +1,31 @@ +@import "../../../colors"; + +.password-complexity-indicator { + margin-top: 12px; + width: 100%; + height: 6px; + border-radius: 5px; + background-color: #eee; + + .bar { + border-radius: 3px; + transition: all 0.25s ease-in; + width: 0%; + height: 6px; + + &.complexity-0, + &.complexity-1, + &.complexity-2 { + background-color: $color-red; + } + + &.complexity-3 { + background-color: $color-yellow; + } + + &.complexity-4, + &.complexity-5 { + background-color: $color-green; + } + } +} diff --git a/src/colors.scss b/src/colors.scss index 93fe90ec..2b5ec40e 100644 --- a/src/colors.scss +++ b/src/colors.scss @@ -48,6 +48,8 @@ $color-grey: #cbcbcb; $color-red-light: #f9c5cc; $color-red: #ff586e; $color-red-dark: #fd334e; +$color-yellow: #ffeb3b; +$color-yellow-dark: #c8b900; $color-green-light: #c8f1e5; $color-green: #0acf97; $color-green-dark: #06b483; diff --git a/src/i18n/locales/en/default.json b/src/i18n/locales/en/default.json index e37ae53b..4e701f73 100644 --- a/src/i18n/locales/en/default.json +++ b/src/i18n/locales/en/default.json @@ -328,6 +328,7 @@ "emailInUse": "The email is already used", "emailNotValid": "Not a valid email address", "passwordsDontMatch": "The passwords are not the same", + "passwordsNotComplex": "Password not complex enough", "alreadyHaveAccount": "Already have an account?", "readPrivacy": "I have read the [Privacy Policy]", "acceptTerms": "I accept the [Terms and Conditions]" diff --git a/src/screens/Login/Login.scss b/src/screens/Login/Login.scss index 6274071e..92dd3134 100644 --- a/src/screens/Login/Login.scss +++ b/src/screens/Login/Login.scss @@ -1,9 +1,6 @@ @import "../../colors"; .login-form { - max-width: 450px; - max-height: 550px; - margin: auto; border-radius: 10px; diff --git a/src/screens/Profile/Profile.jsx b/src/screens/Profile/Profile.jsx index f4973c73..9cb837dc 100644 --- a/src/screens/Profile/Profile.jsx +++ b/src/screens/Profile/Profile.jsx @@ -7,12 +7,14 @@ import PersonIcon from "@material-ui/icons/Person"; import Button from "../../Components/Button/Button.jsx"; import ConfirmDialog from "../../Components/ConfirmDialog/ConfirmDialog.jsx"; import TextInput from "../../Components/Form/TextInput/TextInput.jsx"; +import PasswordComplexityIndicator from "../../Components/Indicators/PasswordComplexityIndicator/PasswordComplexityIndicator.jsx"; import Modal from "../../Components/Modal/Modal.jsx"; import StatsTable from "../../Components/StatsTable/StatsTable.jsx"; import useSnack from "../../hooks/useSnack.js"; import { signOut } from "../../redux/Actions/login.js"; import { deleteUser, changePassword } from "../../utils/api.js"; +import { bytesLength } from "../../utils/index.js"; import "./Profile.scss"; @@ -32,6 +34,8 @@ const Profile = () => { const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); + const [passwordComplexity, setPasswordComplexity] = useState(0); + const [isPasswordComplexityOk, setIsPasswordComplexityOk] = useState(false); const [repeatedNewPassword, setRepeatedNewPassword] = useState(""); const [canSubmitPasswordChange, setCanSubmitPasswordChange] = useState(false); @@ -47,6 +51,8 @@ const Profile = () => { setOldPassword(""); setNewPassword(""); setRepeatedNewPassword(""); + setPasswordComplexity(0); + setIsPasswordComplexityOk(false); }, []); const openEmailModal = useCallback(() => { @@ -93,15 +99,24 @@ const Profile = () => { }); }, [dispatch]); + useEffect(() => { + const passwordLength = bytesLength(newPassword); + + setIsPasswordComplexityOk( + passwordLength >= 8 && passwordLength <= 72 && passwordComplexity >= 4 + ); + }, [newPassword, passwordComplexity]); + useEffect(() => { setCanSubmitPasswordChange( newPassword === repeatedNewPassword && newPassword !== "" && repeatedNewPassword !== "" && - oldPassword !== "" + oldPassword !== "" && + isPasswordComplexityOk ); setNewPasswordError(newPassword !== repeatedNewPassword); - }, [newPassword, oldPassword, repeatedNewPassword]); + }, [isPasswordComplexityOk, newPassword, oldPassword, repeatedNewPassword]); useEffect(() => { setCanSubmitDelete(deleteConfirmation === username); @@ -193,6 +208,13 @@ const Profile = () => { setNewPassword(value); }} value={newPassword} + error={newPassword !== "" && !isPasswordComplexityOk} + errorText={t("screens.register.passwordsNotComplex")} + /> + { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [passwordRepeat, setPasswordRepeat] = useState(""); + const [passwordComplexity, setPasswordComplexity] = useState(0); + const [isPasswordComplexityOk, setIsPasswordComplexityOk] = useState(false); const [inviteCode, setInviteCode] = useState(""); const [serverAddressInput, setServerAddressInput] = useState(serverAddress); const [isSamePassword, setIsSamePassword] = useState(true); @@ -154,6 +158,14 @@ const Register = ({ image }) => { ] ); + useEffect(() => { + const passwordLength = bytesLength(password); + + setIsPasswordComplexityOk( + passwordLength >= 8 && passwordLength <= 72 && passwordComplexity >= 4 + ); + }, [password, passwordComplexity]); + useEffect(() => { setIsSamePassword(password === passwordRepeat); }, [password, passwordRepeat]); @@ -188,6 +200,7 @@ const Register = ({ image }) => { !email || !password || !passwordRepeat || + !isPasswordComplexityOk || !isServerValid || !isSamePassword || !isEmailValid || @@ -214,6 +227,7 @@ const Register = ({ image }) => { acceptTerms, isPrivacyPolicyValid, isTermsAndConditionsValid, + isPasswordComplexityOk, ]); useEffect(() => { @@ -298,12 +312,22 @@ const Register = ({ image }) => { }} value={password} error={ - !isSamePassword && password !== "" && passwordRepeat !== "" + (!isSamePassword && password !== "" && passwordRepeat !== "") || + !isPasswordComplexityOk + } + errorText={ + !isPasswordComplexityOk + ? t("screens.register.passwordsNotComplex") + : t("screens.register.passwordsDontMatch") } - errorText={t("screens.register.passwordsDontMatch")} maxLength={maxTextfieldLength} minLength={8} /> + { return Math.floor(timeDiff / (1000 * 60 * 60 * 24)); }; + +/** + * Return the length of a string in bytes + * See: https://stackoverflow.com/questions/5515869/string-length-in-bytes-in-javascript + * @param {String} string string + * @returns bytesLength + */ +export const bytesLength = (string) => new TextEncoder().encode(string).length;