diff --git a/client/package-lock.json b/client/package-lock.json index 6fbca47..dd8c47b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12832,6 +12832,11 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" }, + "react-hook-form": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.5.tgz", + "integrity": "sha512-so2jEPYKdVk1olMo+HQ9D9n1hVzaPPFO4wsjgSeZ964R7q7CHsYRbVF0PGBi83FcycA5482WHflasdwLIUVENg==" + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/client/package.json b/client/package.json index f9d52a5..bcea957 100644 --- a/client/package.json +++ b/client/package.json @@ -20,6 +20,7 @@ "axios": "^0.21.1", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-hook-form": "^6.15.5", "react-router-dom": "^5.2.0", "react-scripts": "4.0.2", "react-typed": "^1.2.0", diff --git a/client/src/components/Form/Form.tsx b/client/src/components/Form/Form.tsx index f8b228a..ffc0700 100644 --- a/client/src/components/Form/Form.tsx +++ b/client/src/components/Form/Form.tsx @@ -1,18 +1,20 @@ -import { Checkbox, FormControlLabel, Icon, Theme } from '@material-ui/core'; +import { Checkbox, FormControlLabel, Icon, Theme, TextField } from '@material-ui/core'; import { StyledButtonGoogle, StyledForm, StyledTextField, StyledLink, StyledButton, - orParagraph, - StyledFormControlLabel + StyledParagraph, + StyledFormControlLabel, + StyledError } from './styles'; -import React, { FormEvent } from 'react'; +import React, { FormEvent, useEffect } from 'react'; import { css } from '@emotion/react'; import { Grid } from './styles'; import { Search } from '@trejgun/material-ui-icons-google'; import { Container } from '@material-ui/core'; +import { useForm, Controller } from 'react-hook-form'; interface FormProps { isregister?: boolean; @@ -20,6 +22,14 @@ interface FormProps { onEmailChange: (e: string) => void; onPasswordChange: (e: string) => void; onSubmit: (e: FormEvent) => void; + error?: any; +} + +interface IFormInput { + usernameInput: string; + emailInput: string; + passwordInput: string; + termsInput: string; } const handleGoogleRedirect = () => { @@ -38,7 +48,32 @@ const handleGoogleRedirect = () => { }); }; -const Form = ({ isregister, onUsernameChange, onEmailChange, onPasswordChange, onSubmit }: FormProps) => { +const Form = ({ error, isregister, onUsernameChange, onEmailChange, onPasswordChange, onSubmit }: FormProps) => { + const { handleSubmit, control, errors, watch } = useForm(); + const emailWatch: string = watch(`emailInput`); + const usernameWatch: string = watch(`usernameInput`); + const passwordWatch: string = watch(`passwordInput`); + + const passwordMatching = (value: string) => { + if (isregister) { + return value.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,1024}$/) || ''; + } else { + return true; + } + }; + + useEffect(() => { + onEmailChange(emailWatch); + }, [emailWatch]); + + useEffect(() => { + onPasswordChange(passwordWatch); + }, [passwordWatch]); + + useEffect(() => { + if (onUsernameChange) onUsernameChange(usernameWatch); + }, [usernameWatch]); + return ( <> @@ -51,63 +86,135 @@ const Form = ({ isregister, onUsernameChange, onEmailChange, onPasswordChange, o spacing={1} mt={4} > - - handleGoogleRedirect()} variant="outlined"> - - Kontynuuj przez Google - -

LUB

- onSubmit(e)} noValidate autoComplete="off"> - onUsernameChange && onUsernameChange(e.target.value)} - /> - onEmailChange(e.target.value)} - /> - onPasswordChange(e.target.value)} - /> - + handleGoogleRedirect()} variant="outlined"> + + Kontynuuj przez Google + +

LUB

+ + ( + + )} + name="usernameInput" + control={control} + defaultValue="" + rules={{ + required: { + value: isregister || false, + message: 'Nazwa użytkownika/czki jest wymagana' + }, + minLength: { + value: 2, + message: 'Nazwa użytkownika/czki jest za krótka' + }, + maxLength: { + value: 30, + message: 'Nazwa użytkownika/czki jest za długa' + } + }} + /> + {errors.usernameInput && {errors.usernameInput.message}} + ( + + )} + control={control} + name="emailInput" + defaultValue="" + rules={{ + required: { + value: true, + message: 'Email jest wymagany' + }, + pattern: { + value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, + message: 'Wprowadzony email jest niepoprawny' } - label="Akceptuję warunki korzystania i politykę prywatności FiszQI" - /> - - {isregister ? `ZAREJESTRUJ` : 'ZALOGUJ SIĘ'} - - - - {isregister ? `Masz już konto? Zaloguj się` : `Nie masz konta? Zarejestruj się`} - -
+ }} + /> + {errors.emailInput && {errors.emailInput.message}} + ( + + )} + name="passwordInput" + control={control} + rules={{ + required: { + value: true, + message: 'Hasło jest wymagane' + }, + validate: passwordMatching + }} + defaultValue="" + /> + {errors.passwordInput && {errors.passwordInput.message}} + {errors.passwordInput && errors.passwordInput.type === 'validate' && ( + + Hasło powinno składać się z min. 8 znaków zawierać duże i małe litery, liczbę oraz znak + specjalny + + )} + ( + + )} + /> + } + isdisplay={isregister?.toString()} + label="Akceptuję warunki korzystania i politykę prywatności FiszQI" + /> + {errors.termsInput && {errors.termsInput.message}} + {error?.server && {error?.server.message}} + + {isregister ? `ZAREJESTRUJ` : 'ZALOGUJ SIĘ'} + + + + {isregister ? `Masz już konto? Zaloguj się` : `Nie masz konta? Zarejestruj się`} +
diff --git a/client/src/components/Form/styles.tsx b/client/src/components/Form/styles.tsx index 82f2134..67d1c65 100644 --- a/client/src/components/Form/styles.tsx +++ b/client/src/components/Form/styles.tsx @@ -10,6 +10,12 @@ interface FormStylesProps { isdisplay?: string; } +export const StyledError = styled.div` + width: 90%; + margin-left: 5%; + color: red; +`; + export const StyledTextField = styled(TextField)` && { width: 90%; @@ -29,7 +35,7 @@ export const StyledTextField = styled(TextField)` export const StyledFormControlLabel = styled(FormControlLabel)` && { ${(props: FormStylesProps) => (props.isdisplay === 'true' ? '' : 'display: none')}; - width: 80%; + width: 90%; text-align: center; padding: 5px; } @@ -86,7 +92,7 @@ export const StyledButtonGoogle = styled(Button)` } `; -export const orParagraph = css` +export const StyledParagraph = css` text-transform: uppercase; margin: 2vh; font-weight: 700; diff --git a/client/src/views/Login/Login.tsx b/client/src/views/Login/Login.tsx index 2506e23..bf20f0b 100644 --- a/client/src/views/Login/Login.tsx +++ b/client/src/views/Login/Login.tsx @@ -1,19 +1,19 @@ import React, { useState } from 'react'; import Form from '../../components/Form/Form'; import axios from 'axios'; +import { useForm } from 'react-hook-form'; const Login = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - + const { errors, setError } = useForm(); + const handleSubmit = () => { const user = { email: email, password: password }; - axios('/api/register', { + axios('/api/login', { method: 'POST', headers: { 'content-type': 'application/json' @@ -22,13 +22,23 @@ const Login = () => { }) .then((response) => (window.location.href = `/`)) .catch((error) => { + setError('server', { + type: 'server', + message: error.response.data.message + }); throw error; }); }; return ( <> -
+ ); }; diff --git a/client/src/views/Register/Register.tsx b/client/src/views/Register/Register.tsx index db2fc19..453de41 100644 --- a/client/src/views/Register/Register.tsx +++ b/client/src/views/Register/Register.tsx @@ -1,14 +1,14 @@ import React, { useState } from 'react'; import Form from '../../components/Form/Form'; import axios from 'axios'; +import { useForm } from 'react-hook-form'; const Register = () => { const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - + const { errors, setError } = useForm(); + const handleSubmit = () => { const user = { username: username, email: email, @@ -24,7 +24,11 @@ const Register = () => { }) .then((response) => (window.location.href = `/login`)) .catch((error) => { - throw error; + const errors = error.response.data; + setError('server', { + type: 'server', + message: errors + }); }); }; @@ -36,6 +40,7 @@ const Register = () => { onPasswordChange={setPassword} onSubmit={handleSubmit} isregister + error={errors} /> ); diff --git a/server/src/middleware/passport.ts b/server/src/middleware/passport.ts index 79723a3..b8d7243 100644 --- a/server/src/middleware/passport.ts +++ b/server/src/middleware/passport.ts @@ -17,12 +17,12 @@ passport.use( }, async (email: string, password: string, done) => { const user = await User.findOne({ email: email }); - if (!user) return done(null, false, { message: 'User with provided email does not exist.' }); + if (!user) return done(null, false, { message: 'Użytkownik/czka o podanym identyfikatorze nie istnieje' }); bcrypt.compare(password, user.password, (error, isValid) => { if (error) throw error; if (!isValid) { - return done(null, false, { message: 'Invalid email or password' }); + return done(null, false, { message: 'Nieprawidłowy email lub hasło' }); } else { return done(null, user); } diff --git a/server/src/routes/login.ts b/server/src/routes/login.ts index 42fea02..ae4a72f 100644 --- a/server/src/routes/login.ts +++ b/server/src/routes/login.ts @@ -12,7 +12,7 @@ router.post('/', (req: Request, res: Response, next: NextFunction) => { passport.authenticate('local', { session: false }, (err, user, info) => { if (err || !user) { return res.status(401).json({ - message: info ? info.message : 'Login failed', + message: info ? info.message : 'Logowanie nie powiodło się', user: user }); } diff --git a/server/src/routes/register.ts b/server/src/routes/register.ts index e16969d..45c0424 100644 --- a/server/src/routes/register.ts +++ b/server/src/routes/register.ts @@ -10,10 +10,10 @@ router.post('/', async (req: Request, res: Response, next: NextFunction) => { if (error) return res.status(400).send(error.details[0].message); let user = await User.findOne({ email: req.body.email }); - if (user) return res.status(400).send('User already registered'); + if (user) return res.status(400).send('Użytkownik/czka już istnieje'); const userName = await User.findOne({ username: req.body.username }); - if (userName) return res.status(400).send('Username already used'); + if (userName) return res.status(400).send('Nazwa użytkownika/czki już istnieje'); const salt = await bcrypt.genSalt(10); user = new User({