diff --git a/src/components/LoginForm/index.tsx b/src/components/LoginForm/index.tsx index 151fce1a..609e70fe 100644 --- a/src/components/LoginForm/index.tsx +++ b/src/components/LoginForm/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { ChangeEvent, FormEvent, KeyboardEvent, useState } from 'react'; import LoadingButton from '@mui/lab/LoadingButton'; import Grid from '@mui/material/Grid'; @@ -6,21 +7,65 @@ import Typography from '@mui/material/Typography'; import crypto from 'crypto'; import { Address4 } from 'ip-address'; import { useRouter } from 'next/router'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { SessionState, usePostAuthSessionMutation } from '@redux/AuthSession'; +type FormValues = { + ipAddress: string; + password: string; + port: number; +}; + +/** + * Schema used for validating the login form using `zod` + * + * @see {@link https://www.youtube.com/watch?v=RdnJ5UP3HhY&list=PLC3y8-rFHvwjmgBr1327BA5bVXoQH-w5s&index=30&pp=iAQB} + */ +const schema = z.object({ + ipAddress: z + .string() + .min(1, 'IPv4 address is required') + .ip({ version: 'v4', message: 'Invalid IPv4' }), + port: z + .number({ invalid_type_error: 'Must be a number', coerce: true }) + .positive('Port must be greater than 0'), + password: z.string().min(1, 'Password is required'), +}); + /** * Render Login component */ const LoginForm: React.FC = () => { - const [isIpValid, setIpValid] = useState(true); - const [isPortValid, setPortValid] = useState(true); - const [ipAddress, setIpAddress] = useState(''); - const [port, setPort] = useState('80'); - const [password, setPassword] = useState(''); + // const [isIpValid, setIpValid] = useState(true); + // const [isPortValid, setPortValid] = useState(true); + // const [ipAddress, setIpAddress] = useState(''); + // const [port, setPort] = useState('80'); + // const [password, setPassword] = useState(''); const [authMessage, setAuthMessage] = useState(''); const [postAuthSession, { isLoading }] = usePostAuthSessionMutation(); const router = useRouter(); + const form = useForm({ + // https://react-hook-form.com/docs/useform#defaultValues + defaultValues: { + port: 80, + }, + // using zod as a validator + // https://www.youtube.com/watch?v=RdnJ5UP3HhY&list=PLC3y8-rFHvwjmgBr1327BA5bVXoQH-w5s&index=30&pp=iAQB + resolver: zodResolver(schema), + // when to validate + // https://react-hook-form.com/docs/useform#mode + mode: 'all', + }); + + const { handleSubmit, register, formState } = form; + const { + errors, // validations errors returned. Used to display error messages + isDirty, // true if data has changed. + isValid, // true if all validations has passed + } = formState; /** * Check if IPv4 address is valid. @@ -29,13 +74,13 @@ const LoginForm: React.FC = () => { * * Run when there's a change in IP address textfiled */ - const validateIp = () => { - if (Address4.isValid(ipAddress)) { - setIpValid(true); - } else { - setIpValid(false); - } - }; + // const validateIp = () => { + // if (Address4.isValid(ipAddress)) { + // setIpValid(true); + // } else { + // setIpValid(false); + // } + // }; /** * Check port number is valid @@ -44,15 +89,15 @@ const LoginForm: React.FC = () => { * * Run when there's a change in port number textfield */ - const validatePort = () => { - if (port === '') { - setPortValid(false); - } else if (Number.isInteger(+port) && +port > 0) { - setPortValid(true); - } else { - setPortValid(false); - } - }; + // const validatePort = () => { + // if (port === '') { + // setPortValid(false); + // } else if (Number.isInteger(+port) && +port > 0) { + // setPortValid(true); + // } else { + // setPortValid(false); + // } + // }; /** * Set 'ipAddress', 'port', 'password' when their respective textfields change. @@ -60,109 +105,158 @@ const LoginForm: React.FC = () => { * Call 'validateIp' when setting ipAddress * Call 'validatePort' when setting port */ - const onChange = (event: ChangeEvent) => { - const { name, value } = event.target; - - if (name === 'ipAddress') { - setIpAddress(value); - validateIp(); - } else if (name === 'password') { - // store password as a hash. NEVER in plain text - const hash = value.length > 0 ? crypto.createHash('sha256').update(value).digest('hex') : ''; - setPassword(hash); - } else if (name === 'port') { - setPort(value); - validatePort(); - } - }; + // const onChange = (event: ChangeEvent) => { + // const { name, value } = event.target; + // + // if (name === 'ipAddress') { + // setIpAddress(value); + // validateIp(); + // } else if (name === 'password') { + // // store password as a hash. NEVER in plain text + // const hash = value.length > 0 ? crypto.createHash('sha256').update(value).digest('hex') : ''; + // setPassword(hash); + // } else if (name === 'port') { + // setPort(value); + // validatePort(); + // } + // }; /** * Check which key is pressed when typing in port number textfield. * Allow numbers, block anything else */ - const portOnKeyPress = (event: KeyboardEvent) => { - const { which } = event; - - // check the ascii value - // Decimal 0 = 48 ascii - // Decimal 9 = 57 ascii - // obtained from - // https://thewebdev.info/2022/06/15/how-to-prevent-typing-non-numeric-characters-in-input-type-number-with-javascript/ - // - // check if character is a non-numeric - if (which < 48 || which > 57) { - // stop character from appending - event.preventDefault(); - } - }; + // const portOnKeyPress = (event: KeyboardEvent) => { + // const { which } = event; + // + // // check the ascii value + // // Decimal 0 = 48 ascii + // // Decimal 9 = 57 ascii + // // obtained from + // // https://thewebdev.info/2022/06/15/how-to-prevent-typing-non-numeric-characters-in-input-type-number-with-javascript/ + // // + // // check if character is a non-numeric + // if (which < 48 || which > 57) { + // // stop character from appending + // event.preventDefault(); + // } + // }; + + // const onSubmit = async (event: FormEvent) => { + // event.preventDefault(); + // setAuthMessage(''); + // + // try { + // await postAuthSession({ ipAddress, password, port }).unwrap(); + // // replaced `router.push('/').catch(console.error);` because it would not navigate to another page. + // // not sure why this is happening. + // router.reload() + // } catch (err: unknown) { + // setAuthMessage((err as { data: { message: string } }).data.message); + // } + // }; - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - setAuthMessage(''); + const onSubmit = async (data: FormValues) => { + const { ipAddress, port, password } = data; + console.log(data); + + const hashedPassword = crypto.createHash('sha256').update(password).digest('hex'); + console.log('hashedPassword', hashedPassword); try { - await postAuthSession({ ipAddress, password, port }).unwrap(); + await postAuthSession({ ipAddress, password: hashedPassword, port: `${port}` }).unwrap(); // replaced `router.push('/').catch(console.error);` because it would not navigate to another page. // not sure why this is happening. - router.reload() + // router.push('/').catch(console.error); + router.reload(); } catch (err: unknown) { setAuthMessage((err as { data: { message: string } }).data.message); } }; + console.log(`isDirty: ${isDirty}`, `isValid: ${isValid}`); + return ( // eslint-disable-next-line @typescript-eslint/no-misused-promises -
+ { + // return fieldValue.length === 0; + // }, + // }, + })} + error={!!errors.port} + helperText={errors.port?.message} fullWidth - inputProps={{ inputMode: 'numeric', pattern: '\\d*' }} - error={!isPortValid} - {...(!isPortValid ? { helperText: 'Must be a number' } : {})} - value={port} - onBlur={validatePort} - onKeyPress={portOnKeyPress} /> @@ -171,7 +265,7 @@ const LoginForm: React.FC = () => { fullWidth variant='contained' type='submit' - disabled={!ipAddress.length || !isIpValid || password.length === 0 || !isPortValid} + disabled={!isDirty || !isValid} loading={isLoading} > Log in