diff options
author | 2024-06-30 18:55:39 -0400 | |
---|---|---|
committer | 2024-06-30 18:55:39 -0400 | |
commit | 2657469964e24ffbeb905024532120395f6e797c (patch) | |
tree | 2fe9db8a4ecfa92d854ca94f7586d81163c8bd25 /src/views/Authorization | |
download | donetick-frontend-2657469964e24ffbeb905024532120395f6e797c.tar.gz donetick-frontend-2657469964e24ffbeb905024532120395f6e797c.tar.bz2 donetick-frontend-2657469964e24ffbeb905024532120395f6e797c.zip |
move to Donetick Org, First commit frontend
Diffstat (limited to 'src/views/Authorization')
-rw-r--r-- | src/views/Authorization/AuthorizationContainer.jsx | 45 | ||||
-rw-r--r-- | src/views/Authorization/ForgotPasswordView.jsx | 227 | ||||
-rw-r--r-- | src/views/Authorization/LoginView.jsx | 345 | ||||
-rw-r--r-- | src/views/Authorization/Signup.jsx | 243 | ||||
-rw-r--r-- | src/views/Authorization/UpdatePasswordView.jsx | 194 |
5 files changed, 1054 insertions, 0 deletions
diff --git a/src/views/Authorization/AuthorizationContainer.jsx b/src/views/Authorization/AuthorizationContainer.jsx new file mode 100644 index 0000000..3bfc622 --- /dev/null +++ b/src/views/Authorization/AuthorizationContainer.jsx @@ -0,0 +1,45 @@ +// import Logo from 'Components/Logo' +import { Box, Paper } from '@mui/material' +import { styled } from '@mui/material/styles' + +const Container = styled('div')(({ theme }) => ({ + minHeight: '100vh', + padding: '24px', + display: 'grid', + placeItems: 'start center', + [theme.breakpoints.up('sm')]: { + // center children + placeItems: 'center', + }, +})) + +const AuthCard = styled(Paper)(({ theme }) => ({ + // border: "1px solid #c4c4c4", + padding: 24, + paddingTop: 32, + borderRadius: 24, + width: '100%', + maxWidth: '400px', + [theme.breakpoints.down('sm')]: { + maxWidth: 'unset', + }, +})) + +export default function AuthCardContainer({ children, ...props }) { + return ( + <Container> + <AuthCard elevation={0}> + <Box + sx={{ + display: 'grid', + placeItems: 'center', + paddingBottom: 4, + }} + > + {/* <Logo size='96px' /> */} + </Box> + {children} + </AuthCard> + </Container> + ) +} diff --git a/src/views/Authorization/ForgotPasswordView.jsx b/src/views/Authorization/ForgotPasswordView.jsx new file mode 100644 index 0000000..44601eb --- /dev/null +++ b/src/views/Authorization/ForgotPasswordView.jsx @@ -0,0 +1,227 @@ +// create boilerplate for ResetPasswordView: +import { + Box, + Button, + Container, + FormControl, + FormHelperText, + Input, + Sheet, + Snackbar, + Typography, +} from '@mui/joy' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' + +import { API_URL } from './../../Config' + +const ForgotPasswordView = () => { + const navigate = useNavigate() + // const [showLoginSnackbar, setShowLoginSnackbar] = useState(false) + // const [snackbarMessage, setSnackbarMessage] = useState('') + const [resetStatusOk, setResetStatusOk] = useState(null) + const [email, setEmail] = useState('') + const [emailError, setEmailError] = useState(null) + + const validateEmail = email => { + return !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email) + } + + const handleSubmit = async () => { + if (!email) { + return setEmailError('Email is required') + } + + // validate email: + if (validateEmail(email)) { + setEmailError('Please enter a valid email address') + return + } + + if (emailError) { + return + } + + try { + const response = await fetch(`${API_URL}/auth/reset`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email: email }), + }) + + if (response.ok) { + setResetStatusOk(true) + // wait 3 seconds and then redirect to login: + } else { + setResetStatusOk(false) + } + } catch (error) { + setResetStatusOk(false) + } + } + + const handleEmailChange = e => { + setEmail(e.target.value) + if (validateEmail(e.target.value)) { + setEmailError('Please enter a valid email address') + } else { + setEmailError(null) + } + } + + return ( + <Container + component='main' + maxWidth='xs' + + // make content center in the middle of the page: + > + <Box + sx={{ + marginTop: 4, + display: 'flex', + flexDirection: 'column', + + justifyContent: 'space-between', + alignItems: 'center', + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + minHeight: '70vh', + justifyContent: 'space-between', + justifyItems: 'center', + }} + > + <Box> + <img + src='/src/assets/logo.svg' + alt='logo' + width='128px' + height='128px' + /> + {/* <Logo /> */} + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + </Box> + {/* HERE */} + <Box sx={{ textAlign: 'center' }}></Box> + {resetStatusOk === null && ( + <form onSubmit={handleSubmit}> + <div className='grid gap-6'> + <Typography level='body2' gutterBottom> + Enter your email, and we'll send you a link to get into your + account. + </Typography> + <FormControl error={emailError !== null}> + <Input + placeholder='Email' + type='email' + variant='soft' + fullWidth + size='lg' + value={email} + onChange={handleEmailChange} + error={emailError !== null} + onKeyDown={e => { + if (e.key === 'Enter') { + e.preventDefault() + handleSubmit() + } + }} + /> + <FormHelperText>{emailError}</FormHelperText> + </FormControl> + <Box> + <Button + variant='solid' + size='lg' + fullWidth + sx={{ + mb: 1, + }} + onClick={handleSubmit} + > + Reset Password + </Button> + <Button + fullWidth + size='lg' + variant='soft' + sx={{ + width: '100%', + border: 'moccasin', + borderRadius: '8px', + }} + onClick={() => { + navigate('/login') + }} + color='neutral' + > + Back to Login + </Button> + </Box> + </div> + </form> + )} + {resetStatusOk != null && ( + <> + <Box mt={-30}> + <Typography level='body-md'> + if there is an account associated with the email you entered, + you will receive an email with instructions on how to reset + your + </Typography> + </Box> + <Button + variant='soft' + size='lg' + sx={{ position: 'relative', bottom: '0' }} + onClick={() => { + navigate('/login') + }} + fullWidth + > + Go to Login + </Button> + </> + )} + <Snackbar + open={resetStatusOk ? resetStatusOk : resetStatusOk === false} + autoHideDuration={5000} + onClose={() => { + if (resetStatusOk) { + navigate('/login') + } + }} + > + {resetStatusOk + ? 'Reset email sent, check your email' + : 'Reset email failed, try again later'} + </Snackbar> + </Sheet> + </Box> + </Container> + ) +} + +export default ForgotPasswordView diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx new file mode 100644 index 0000000..2ffcef4 --- /dev/null +++ b/src/views/Authorization/LoginView.jsx @@ -0,0 +1,345 @@ +import GoogleIcon from '@mui/icons-material/Google' +import { + Avatar, + Box, + Button, + Container, + Divider, + Input, + Sheet, + Snackbar, + Typography, +} from '@mui/joy' +import Cookies from 'js-cookie' +import React from 'react' +import { useNavigate } from 'react-router-dom' +import { LoginSocialGoogle } from 'reactjs-social-login' +import { API_URL, GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config' +import { UserContext } from '../../contexts/UserContext' +import Logo from '../../Logo' +import { GetUserProfile } from '../../utils/Fetcher' +const LoginView = () => { + const { userProfile, setUserProfile } = React.useContext(UserContext) + const [username, setUsername] = React.useState('') + const [password, setPassword] = React.useState('') + const [error, setError] = React.useState(null) + const Navigate = useNavigate() + const handleSubmit = async e => { + e.preventDefault() + + fetch(`${API_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }) + .then(response => { + if (response.status === 200) { + return response.json().then(data => { + localStorage.setItem('ca_token', data.token) + localStorage.setItem('ca_expiration', data.expire) + const redirectUrl = Cookies.get('ca_redirect') + // console.log('redirectUrl', redirectUrl) + if (redirectUrl) { + Cookies.remove('ca_redirect') + Navigate(redirectUrl) + } else { + Navigate('/my/chores') + } + }) + } else if (response.status === 401) { + setError('Wrong username or password') + } else { + setError('An error occurred, please try again') + console.log('Login failed') + } + }) + .catch(err => { + setError('Unable to communicate with server, please try again') + console.log('Login failed', err) + }) + } + + const loggedWithProvider = function (provider, data) { + console.log(provider, data) + return fetch(API_URL + `/auth/${provider}/callback`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + provider: provider, + token: + data['access_token'] || // data["access_token"] is for Google + data['accessToken'], // data["accessToken"] is for Facebook + data: data, + }), + }).then(response => { + if (response.status === 200) { + return response.json().then(data => { + localStorage.setItem('ca_token', data.token) + localStorage.setItem('ca_expiration', data.expire) + // setIsLoggedIn(true); + getUserProfileAndNavigateToHome() + }) + } + return response.json().then(error => { + setError("Couldn't log in with Google, please try again") + }) + }) + } + const getUserProfileAndNavigateToHome = () => { + GetUserProfile().then(data => { + data.json().then(data => { + setUserProfile(data.res) + // check if redirect url is set in cookie: + const redirectUrl = Cookies.get('ca_redirect') + if (redirectUrl) { + Cookies.remove('ca_redirect') + Navigate(redirectUrl) + } else { + Navigate('/my/chores') + } + }) + }) + } + const handleForgotPassword = () => { + Navigate('/forgot-password') + } + return ( + <Container + component='main' + maxWidth='xs' + + // make content center in the middle of the page: + > + <Box + sx={{ + marginTop: 4, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + {/* <img + src='/src/assets/logo.svg' + alt='logo' + width='128px' + height='128px' + /> */} + <Logo /> + + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + + {userProfile && ( + <> + <Avatar + src={userProfile?.image} + alt={userProfile?.username} + size='lg' + sx={{ + mt: 2, + width: '96px', + height: '96px', + mb: 1, + }} + /> + <Typography level='body-md' alignSelf={'center'}> + Welcome back,{' '} + {userProfile?.displayName || userProfile?.username} + </Typography> + + <Button + fullWidth + size='lg' + sx={{ mt: 3, mb: 2 }} + onClick={() => { + getUserProfileAndNavigateToHome() + }} + > + Continue as {userProfile.displayName || userProfile.username} + </Button> + <Button + type='submit' + fullWidth + size='lg' + q + variant='plain' + sx={{ + width: '100%', + mb: 2, + border: 'moccasin', + borderRadius: '8px', + }} + onClick={() => { + setUserProfile(null) + localStorage.removeItem('ca_token') + localStorage.removeItem('ca_expiration') + // go to login page: + window.location.href = '/login' + }} + > + Logout + </Button> + </> + )} + {!userProfile && ( + <> + <Typography level='body2'> + Sign in to your account to continue + </Typography> + <Typography level='body2' alignSelf={'start'} mt={4}> + Username + </Typography> + <Input + margin='normal' + required + fullWidth + id='email' + label='Email Address' + name='email' + autoComplete='email' + autoFocus + value={username} + onChange={e => { + setUsername(e.target.value) + }} + /> + <Typography level='body2' alignSelf={'start'}> + Password: + </Typography> + <Input + margin='normal' + required + fullWidth + name='password' + label='Password' + type='password' + id='password' + value={password} + onChange={e => { + setPassword(e.target.value) + }} + /> + + <Button + type='submit' + fullWidth + size='lg' + variant='solid' + sx={{ + width: '100%', + mt: 3, + mb: 2, + border: 'moccasin', + borderRadius: '8px', + }} + onClick={handleSubmit} + > + Sign In + </Button> + <Button + type='submit' + fullWidth + size='lg' + q + variant='plain' + sx={{ + width: '100%', + mb: 2, + border: 'moccasin', + borderRadius: '8px', + }} + onClick={handleForgotPassword} + > + Forgot password? + </Button> + </> + )} + <Divider> or </Divider> + + <Box sx={{ width: '100%' }}> + <LoginSocialGoogle + client_id={GOOGLE_CLIENT_ID} + redirect_uri={REDIRECT_URL} + scope='openid profile email' + discoveryDocs='claims_supported' + access_type='online' + isOnlyGetToken={true} + onResolve={({ provider, data }) => { + loggedWithProvider(provider, data) + }} + onReject={err => { + setError("Couldn't log in with Google, please try again") + }} + > + <Button + variant='soft' + color='neutral' + size='lg' + fullWidth + sx={{ + width: '100%', + mt: 1, + mb: 1, + border: 'moccasin', + borderRadius: '8px', + }} + > + <div className='flex gap-2'> + <GoogleIcon /> + Continue with Google + </div> + </Button> + </LoginSocialGoogle> + </Box> + + <Button + onClick={() => { + Navigate('/signup') + }} + fullWidth + variant='soft' + size='lg' + // sx={{ mt: 3, mb: 2 }} + > + Create new account + </Button> + </Sheet> + </Box> + <Snackbar + open={error !== null} + onClose={() => setError(null)} + autoHideDuration={3000} + message={error} + > + {error} + </Snackbar> + </Container> + ) +} + +export default LoginView diff --git a/src/views/Authorization/Signup.jsx b/src/views/Authorization/Signup.jsx new file mode 100644 index 0000000..d83411f --- /dev/null +++ b/src/views/Authorization/Signup.jsx @@ -0,0 +1,243 @@ +import { + Box, + Button, + Container, + Divider, + FormControl, + FormHelperText, + Input, + Sheet, + Typography, +} from '@mui/joy' +import React from 'react' +import { useNavigate } from 'react-router-dom' +import Logo from '../../Logo' +import { login, signUp } from '../../utils/Fetcher' + +const SignupView = () => { + const [username, setUsername] = React.useState('') + const [password, setPassword] = React.useState('') + const Navigate = useNavigate() + const [displayName, setDisplayName] = React.useState('') + const [email, setEmail] = React.useState('') + const [usernameError, setUsernameError] = React.useState('') + const [passwordError, setPasswordError] = React.useState('') + const [emailError, setEmailError] = React.useState('') + const [displayNameError, setDisplayNameError] = React.useState('') + const [error, setError] = React.useState(null) + const handleLogin = (username, password) => { + login(username, password).then(response => { + if (response.status === 200) { + response.json().then(res => { + localStorage.setItem('ca_token', res.token) + localStorage.setItem('ca_expiration', res.expire) + setTimeout(() => { + // TODO: not sure if there is a race condition here + // but on first sign up it renavigates to login. + Navigate('/my/chores') + }, 500) + }) + } else { + console.log('Login failed', response) + // Navigate('/login') + } + }) + } + const handleSignUpValidation = () => { + // Reset errors before validation + setUsernameError(null) + setPasswordError(null) + setDisplayNameError(null) + setEmailError(null) + + let isValid = true + + if (!username.trim()) { + setUsernameError('Username is required') + isValid = false + } + if (username.length < 4) { + setUsernameError('Username must be at least 4 characters') + isValid = false + } + // if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + // setEmailError('Invalid email address') + // isValid = false + // } + + if (password.length < 8) { + setPasswordError('Password must be at least 8 characters') + isValid = false + } + + if (!displayName.trim()) { + setDisplayNameError('Display name is required') + isValid = false + } + + // display name should only contain letters and spaces and numbers: + if (!/^[a-zA-Z0-9 ]+$/.test(displayName)) { + setDisplayNameError('Display name can only contain letters and numbers') + isValid = false + } + + // username should only contain letters , numbers , dot and dash: + if (!/^[a-zA-Z0-9.-]+$/.test(username)) { + setUsernameError( + 'Username can only contain letters, numbers, dot and dash', + ) + isValid = false + } + + return isValid + } + const handleSubmit = async e => { + e.preventDefault() + if (!handleSignUpValidation()) { + return + } + signUp(username, password, displayName, email).then(response => { + if (response.status === 201) { + handleLogin(username, password) + } else { + console.log('Signup failed') + setError('Signup failed') + } + }) + } + + return ( + <Container component='main' maxWidth='xs'> + <Box + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginTop: 4, + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + // alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + <Box + sx={{ + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + }} + > + <Logo /> + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + <Typography level='body2'> + Create an account to get started! + </Typography> + </Box> + <Typography level='body2' alignSelf={'start'} mt={4}> + Username + </Typography> + <Input + margin='normal' + required + fullWidth + id='email' + label='Email Address' + name='email' + autoComplete='email' + autoFocus + value={username} + onChange={e => { + setUsernameError(null) + setUsername(e.target.value.trim()) + }} + /> + <FormControl error={usernameError}> + <FormHelperText c>{usernameError}</FormHelperText> + </FormControl> + {/* Error message display */} + <Typography level='body2' alignSelf={'start'}> + Password: + </Typography> + <Input + margin='normal' + required + fullWidth + name='password' + label='Password' + type='password' + id='password' + value={password} + onChange={e => { + setPasswordError(null) + setPassword(e.target.value) + }} + /> + <FormControl error={passwordError}> + <FormHelperText>{passwordError}</FormHelperText> + </FormControl> + <Typography level='body2' alignSelf={'start'}> + Display Name: + </Typography> + <Input + margin='normal' + required + fullWidth + name='displayName' + label='Display Name' + id='displayName' + value={displayName} + onChange={e => { + setDisplayNameError(null) + setDisplayName(e.target.value) + }} + /> + <FormControl error={displayNameError}> + <FormHelperText>{displayNameError}</FormHelperText> + </FormControl> + <Button + // type='submit' + size='lg' + fullWidth + variant='solid' + sx={{ mt: 3, mb: 1 }} + onClick={handleSubmit} + > + Sign Up + </Button> + <Divider> or </Divider> + <Button + size='lg' + onClick={() => { + Navigate('/login') + }} + fullWidth + variant='soft' + // sx={{ mt: 3, mb: 2 }} + > + Login + </Button> + </Sheet> + </Box> + </Container> + ) +} + +export default SignupView diff --git a/src/views/Authorization/UpdatePasswordView.jsx b/src/views/Authorization/UpdatePasswordView.jsx new file mode 100644 index 0000000..7177f2f --- /dev/null +++ b/src/views/Authorization/UpdatePasswordView.jsx @@ -0,0 +1,194 @@ +// create boilerplate for ResetPasswordView: +import { + Box, + Button, + Container, + FormControl, + FormHelperText, + Input, + Sheet, + Snackbar, + Typography, +} from '@mui/joy' +import { useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' + +import { API_URL } from '../../Config' +import Logo from '../../Logo' + +const UpdatePasswordView = () => { + const navigate = useNavigate() + const [password, setPassword] = useState('') + const [passwordConfirm, setPasswordConfirm] = useState('') + const [passwordError, setPasswordError] = useState(null) + const [passworConfirmationError, setPasswordConfirmationError] = + useState(null) + const [searchParams] = useSearchParams() + + const [updateStatusOk, setUpdateStatusOk] = useState(null) + + const verifiticationCode = searchParams.get('c') + + const handlePasswordChange = e => { + const password = e.target.value + setPassword(password) + if (password.length < 8) { + setPasswordError('Password must be at least 8 characters') + } else { + setPasswordError(null) + } + } + const handlePasswordConfirmChange = e => { + setPasswordConfirm(e.target.value) + if (e.target.value !== password) { + setPasswordConfirmationError('Passwords do not match') + } else { + setPasswordConfirmationError(null) + } + } + + const handleSubmit = async () => { + if (passwordError != null || passworConfirmationError != null) { + return + } + try { + const response = await fetch( + `${API_URL}/auth/password?c=${verifiticationCode}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ password: password }), + }, + ) + + if (response.ok) { + setUpdateStatusOk(true) + // wait 3 seconds and then redirect to login: + setTimeout(() => { + navigate('/login') + }, 3000) + } else { + setUpdateStatusOk(false) + } + } catch (error) { + setUpdateStatusOk(false) + } + } + return ( + <Container component='main' maxWidth='xs'> + <Box + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginTop: 4, + }} + > + <Sheet + component='form' + sx={{ + mt: 1, + width: '100%', + display: 'flex', + flexDirection: 'column', + // alignItems: 'center', + padding: 2, + borderRadius: '8px', + boxShadow: 'md', + }} + > + <Box + sx={{ + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + }} + > + <Logo /> + <Typography level='h2'> + Done + <span + style={{ + color: '#06b6d4', + }} + > + tick + </span> + </Typography> + <Typography level='body2' mb={4}> + Please enter your new password below + </Typography> + </Box> + + <FormControl error> + <Input + placeholder='Password' + type='password' + value={password} + onChange={handlePasswordChange} + error={passwordError !== null} + // onKeyDown={e => { + // if (e.key === 'Enter' && validateForm(validateFormInput)) { + // handleSubmit(e) + // } + // }} + /> + <FormHelperText>{passwordError}</FormHelperText> + </FormControl> + + <FormControl error> + <Input + placeholder='Confirm Password' + type='password' + value={passwordConfirm} + onChange={handlePasswordConfirmChange} + error={passworConfirmationError !== null} + // onKeyDown={e => { + // if (e.key === 'Enter' && validateForm(validateFormInput)) { + // handleSubmit(e) + // } + // }} + /> + <FormHelperText>{passworConfirmationError}</FormHelperText> + </FormControl> + {/* helper to show password not matching : */} + + <Button + fullWidth + size='lg' + sx={{ + mt: 5, + mb: 1, + }} + onClick={handleSubmit} + > + Save Password + </Button> + <Button + fullWidth + size='lg' + variant='soft' + onClick={() => { + navigate('/login') + }} + > + Cancel + </Button> + </Sheet> + </Box> + <Snackbar + open={updateStatusOk !== true} + autoHideDuration={6000} + onClose={() => { + setUpdateStatusOk(null) + }} + > + Password update failed, try again later + </Snackbar> + </Container> + ) +} + +export default UpdatePasswordView |