diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 076761a2..41859609 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -28,7 +28,8 @@ "react-icons": "^5.0.1", "react-router-dom": "^6.22.3", "react-scripts": "^5.0.1", - "web-vitals": "^3.5.1" + "web-vitals": "^3.5.1", + "zxcvbn": "^4.4.2" }, "devDependencies": { "axios-mock-adapter": "^1.22.0", @@ -28453,6 +28454,11 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==" } } } diff --git a/webapp/package.json b/webapp/package.json index 2ea16917..55422652 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -23,7 +23,8 @@ "react-icons": "^5.0.1", "react-router-dom": "^6.22.3", "react-scripts": "^5.0.1", - "web-vitals": "^3.5.1" + "web-vitals": "^3.5.1", + "zxcvbn": "^4.4.2" }, "scripts": { "start": "react-scripts start", diff --git a/webapp/src/components/loginAndRegistration/AddUser.js b/webapp/src/components/loginAndRegistration/AddUser.js index 2324ecab..0714bd67 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.js +++ b/webapp/src/components/loginAndRegistration/AddUser.js @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import axios from 'axios'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import zxcvbn from "zxcvbn"; const AddUser = () => { @@ -14,26 +15,87 @@ const AddUser = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [repeatPassword, setRepeatPassword] = useState(''); + const [passwordStrength, setPasswordStrength] = useState(undefined); + const [passwordStrengthText, setPasswordStrengthText] = useState(''); + const [submitError, setSubmitError] = useState(''); const handleSubmit = async (event) => { event.preventDefault(); try { - //TODO: Add more validations - if(password === repeatPassword){ //User put the same password + //Validations + //TODO: email validation + if(password !== repeatPassword){ + //User put the same password + setSubmitError("addUser.error_passwords_no_match"); + } else if(/\s/.test(password)){ + //User put spaces in password + setSubmitError("addUser.error_password_spaces"); + } else if(password.length < 8){ + //Password too short + setSubmitError("addUser.error_password_minimum_length"); + } else if(password.length > 64){ + //Password too long + setSubmitError("addUser.error_password_maximum_length"); + } else if(/\s/.test(username)){ + //Spaces in username + setSubmitError("addUser.error_username_spaces"); + } else{ + //Continue + setSubmitError(''); const response = await axios.post(apiUrl, { username, password }); console.log("Registered user: " + response.data.username); navigate('/login'); } - else{ - //TODO: Show some errors to the user - } } catch (error) { + if(error.response.data.error === "Username already in use"){ //TODO: Improve + setSubmitError("addUser.error_username_in_use"); + } console.error('Error adding user:', error); } }; + //Possible email validation + /** + const validateEmail = (email) => { + return String(email) + .toLowerCase() + .match( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ); + }; + */ + + const handlePasswordChange = (e) => { + const newPassword = e.target.value; + setPassword(newPassword); + + const newStrength = zxcvbn(newPassword); + + switch(newStrength.score){ + case 0: + setPasswordStrengthText("addUser.very_weak_password"); + break; + case 1: + setPasswordStrengthText("addUser.very_weak_password"); + break; + case 2: + setPasswordStrengthText("addUser.weak_password"); + break; + case 3: + setPasswordStrengthText("addUser.good_password"); + break; + case 4: + setPasswordStrengthText("addUser.strong_password"); + break; + default: + setPasswordStrengthText("addUser.very_weak_password"); + break; + } + setPasswordStrength(newStrength); + }; + return (
@@ -51,7 +113,7 @@ const AddUser = () => { onChange={(e) => setUsername(e.target.value)} />
-
+

{t("addUser.password_placeholder")}:

{ placeholder={t("addUser.password_placeholder")} required value={password} - onChange={(e) => setPassword(e.target.value)} + onChange={handlePasswordChange} />
+
+ + {t(passwordStrengthText.toString())} + + +

{t("addUser.repeat_password_placeholder")}:

{ onChange={(e) => setRepeatPassword(e.target.value)} />
- + {submitError &&

{t(submitError)}

} diff --git a/webapp/src/components/loginAndRegistration/AddUser.test.js b/webapp/src/components/loginAndRegistration/AddUser.test.js index 8cf04089..70493840 100644 --- a/webapp/src/components/loginAndRegistration/AddUser.test.js +++ b/webapp/src/components/loginAndRegistration/AddUser.test.js @@ -1,20 +1,28 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import AddUser from './AddUser'; -import { BrowserRouter as Router } from 'react-router-dom'; +import axios from 'axios'; +import { BrowserRouter as Router } from 'react-router-dom'; // Mocking useTranslation hook jest.mock('react-i18next', () => ({ useTranslation: () => ({ t: key => key }), })); +// Mocking axios to simulate an error response +jest.mock('axios'); + describe('', () => { - test('renders the AddUser component', () => { + + beforeEach(() => { render( ); + }); + + test('renders the AddUser component', () => { expect(screen.getByText('addUser.title')).toBeInTheDocument(); expect(screen.getByText('addUser.username_placeholder:')).toBeInTheDocument(); @@ -24,6 +32,46 @@ describe('', () => { expect(screen.getByText('addUser.login_link')).toBeInTheDocument(); }); + + const fillFormAndSubmit = (username, password, repeatPassword) => { + const usernameInput = screen.getByPlaceholderText('addUser.username_placeholder'); + fireEvent.change(usernameInput, { target: { value: username } }); + + const passwordInput = screen.getByPlaceholderText('addUser.password_placeholder'); + fireEvent.change(passwordInput, { target: { value: password } }); + + const repeatPasswordInput = screen.getByPlaceholderText('addUser.repeat_password_placeholder'); + fireEvent.change(repeatPasswordInput, { target: { value: repeatPassword } }); + + const submitButton = screen.getByText('addUser.register_button'); + fireEvent.click(submitButton); + }; + + test('displays correct error messages', async () => { + //Passwords do not match + fillFormAndSubmit('username', '12345678', '123456789'); + expect(screen.getByText('addUser.error_passwords_no_match')).toBeInTheDocument(); + //Password with spaces + fillFormAndSubmit('username', '1234 5678', '1234 5678'); + expect(screen.getByText('addUser.error_password_spaces')).toBeInTheDocument(); + //Password too short + fillFormAndSubmit('username', '1234567', '1234567'); + expect(screen.getByText('addUser.error_password_minimum_length')).toBeInTheDocument(); + //Password too long + fillFormAndSubmit('username', '01234567890123456789012345678901234567890123456789012345678901234', '01234567890123456789012345678901234567890123456789012345678901234'); + expect(screen.getByText('addUser.error_password_maximum_length')).toBeInTheDocument(); + //Username with spaces + fillFormAndSubmit('user name', '12345678', '12345678'); + expect(screen.getByText('addUser.error_username_spaces')).toBeInTheDocument(); + //Username in use + axios.post.mockRejectedValue({ response: { data: { error: 'Username already in use' } } }); + fillFormAndSubmit('existing_user', '12345678', '12345678'); + await waitFor(() => { + expect(screen.getByText('addUser.error_username_in_use')).toBeInTheDocument(); + }); + expect(axios.post).toHaveBeenCalledWith(expect.any(String), { username: 'existing_user', password: '12345678' }); + }); + }); diff --git a/webapp/src/custom.css b/webapp/src/custom.css index daa780fd..7abd6447 100644 --- a/webapp/src/custom.css +++ b/webapp/src/custom.css @@ -141,6 +141,44 @@ padding: 10px; } + .input-box-password-register { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 0px; + } + + .input-box-password-register p { + font-weight: bold; + margin-right: 10%; + width: 150px; + text-align: right; + } + + .input-box-password-register input { + flex: 1; + height: 40px; + background: rgb(255, 255, 255); + border: 2px solid gray; + border-radius: 10px; + font-size: 18px; + color: black; + padding: 10px; + padding-bottom: 0px; + } + + .password-strength-meter { + display: flex; + flex-direction: row; + align-items: left; + gap: 20px; + margin-top: 0px; + } + + .password-strength-meter span { + font-size: 14px; + color: rgba(255, 255, 255, 0.5); + } .button-register, .button-login { appearance: none; diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json index 8f79adda..60fd49fa 100644 --- a/webapp/src/translations/en/global.json +++ b/webapp/src/translations/en/global.json @@ -50,7 +50,17 @@ "password_placeholder": "Password", "repeat_password_placeholder": "Repeat password", "register_button": "Register", - "login_link": "Do you have an account? Login here." + "login_link": "Do you have an account? Login here.", + "very_weak_password": "Very weak password", + "weak_password": "Weak password", + "good_password": "Good password", + "strong_password": "Strong password", + "error_passwords_no_match": "Passwords do not match", + "error_password_spaces": "Password cannot contain spaces", + "error_username_spaces": "Username cannot contain spaces", + "error_password_minimum_length": "Password must be at least 8 characters long", + "error_password_maximum_length": "Password cannot be over 64 characters long", + "error_username_in_use": "Username already in use" }, "gameMenu":{ "history_button":"View Historical Data", diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json index 410eebfa..6c6abaa9 100644 --- a/webapp/src/translations/es/global.json +++ b/webapp/src/translations/es/global.json @@ -54,7 +54,17 @@ "password_placeholder": "Contraseña", "repeat_password_placeholder": "Repetir contraseña", "register_button": "Registrarse", - "login_link": "¿Ya tienes una cuenta? Inicia sesión aquí." + "login_link": "¿Ya tienes una cuenta? Inicia sesión aquí.", + "very_weak_password": "Contraseña muy débil", + "weak_password": "Contraseña débil", + "good_password": "Contraseña buena", + "strong_password": "Contraseña fuerte", + "error_passwords_no_match": "Las contraseñas no coinciden", + "error_password_spaces": "La contraseña no puede contener espacios", + "error_username_spaces": "El nombre de usuario no puede contener espacios", + "error_password_minimum_length": "La contraseña debe tener al menos 8 caracteres", + "error_password_maximum_length": "La contraseña no debe tener más de 64 caracteres", + "error_username_in_use": "Nombre de usuario no disponible" }, "gameMenu":{ "history_button":"Ver Historial", diff --git a/webapp/src/translations/tk/global.json b/webapp/src/translations/tk/global.json index 5a17d359..36d4dd1c 100644 --- a/webapp/src/translations/tk/global.json +++ b/webapp/src/translations/tk/global.json @@ -50,7 +50,17 @@ "password_placeholder": "Şifre", "repeat_password_placeholder": "Şifreyi Tekrar Girin", "register_button": "Kayıt Ol", - "login_link": "Hesabınız var mı? Buradan giriş yapın." + "login_link": "Hesabınız var mı? Buradan giriş yapın.", + "very_weak_password": "Çok zayıf şifre", + "weak_password": "Zayıf şifre", + "good_password": "İyi şifre", + "strong_password": "Güçlü şifre", + "error_passwords_no_match": "Şifreler eşleşmiyor", + "error_password_spaces": "Şifre boşluk içeremez", + "error_username_spaces": "Kullanıcı adı boşluk içeremez.", + "error_password_minimum_length": "Şifre en az 8 karakter uzunluğunda olmalıdır", + "error_password_maximum_length": "Şifre en fazla 64 karakter uzunluğunda olabilir", + "error_username_in_use": "Kullanıcı adı zaten kullanımda" }, "gameMenu": { "history_button": "Tarihsel Verileri Görüntüle",