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;