From 2657469964e24ffbeb905024532120395f6e797c Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 30 Jun 2024 18:55:39 -0400 Subject: move to Donetick Org, First commit frontend --- src/views/Authorization/AuthorizationContainer.jsx | 45 +++ src/views/Authorization/ForgotPasswordView.jsx | 227 ++++++++++++++ src/views/Authorization/LoginView.jsx | 345 +++++++++++++++++++++ src/views/Authorization/Signup.jsx | 243 +++++++++++++++ src/views/Authorization/UpdatePasswordView.jsx | 194 ++++++++++++ 5 files changed, 1054 insertions(+) create mode 100644 src/views/Authorization/AuthorizationContainer.jsx create mode 100644 src/views/Authorization/ForgotPasswordView.jsx create mode 100644 src/views/Authorization/LoginView.jsx create mode 100644 src/views/Authorization/Signup.jsx create mode 100644 src/views/Authorization/UpdatePasswordView.jsx (limited to 'src/views/Authorization') 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 ( + + + + {/* */} + + {children} + + + ) +} 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 ( + + + + + logo + {/* */} + + Done + + tick + + + + {/* HERE */} + + {resetStatusOk === null && ( +
+
+ + Enter your email, and we'll send you a link to get into your + account. + + + { + if (e.key === 'Enter') { + e.preventDefault() + handleSubmit() + } + }} + /> + {emailError} + + + + + +
+
+ )} + {resetStatusOk != null && ( + <> + + + if there is an account associated with the email you entered, + you will receive an email with instructions on how to reset + your + + + + + )} + { + if (resetStatusOk) { + navigate('/login') + } + }} + > + {resetStatusOk + ? 'Reset email sent, check your email' + : 'Reset email failed, try again later'} + +
+
+
+ ) +} + +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 ( + + + + {/* logo */} + + + + Done + + tick + + + + {userProfile && ( + <> + + + Welcome back,{' '} + {userProfile?.displayName || userProfile?.username} + + + + + + )} + {!userProfile && ( + <> + + Sign in to your account to continue + + + Username + + { + setUsername(e.target.value) + }} + /> + + Password: + + { + setPassword(e.target.value) + }} + /> + + + + + )} + or + + + { + loggedWithProvider(provider, data) + }} + onReject={err => { + setError("Couldn't log in with Google, please try again") + }} + > + + + + + + + + setError(null)} + autoHideDuration={3000} + message={error} + > + {error} + + + ) +} + +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 ( + + + + + + + Done + + tick + + + + Create an account to get started! + + + + Username + + { + setUsernameError(null) + setUsername(e.target.value.trim()) + }} + /> + + {usernameError} + + {/* Error message display */} + + Password: + + { + setPasswordError(null) + setPassword(e.target.value) + }} + /> + + {passwordError} + + + Display Name: + + { + setDisplayNameError(null) + setDisplayName(e.target.value) + }} + /> + + {displayNameError} + + + or + + + + + ) +} + +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 ( + + + + + + + Done + + tick + + + + Please enter your new password below + + + + + { + // if (e.key === 'Enter' && validateForm(validateFormInput)) { + // handleSubmit(e) + // } + // }} + /> + {passwordError} + + + + { + // if (e.key === 'Enter' && validateForm(validateFormInput)) { + // handleSubmit(e) + // } + // }} + /> + {passworConfirmationError} + + {/* helper to show password not matching : */} + + + + + + { + setUpdateStatusOk(null) + }} + > + Password update failed, try again later + + + ) +} + +export default UpdatePasswordView -- cgit