diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 5d049a87..00000000 --- a/.husky/pre-push +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -cd server && npm run checks && cd ../client && npm run checks && cd .. diff --git a/client/src/components/BannerSection.tsx b/client/src/components/BannerSection.tsx index f217b4b2..5af64d16 100644 --- a/client/src/components/BannerSection.tsx +++ b/client/src/components/BannerSection.tsx @@ -1,3 +1,4 @@ +import React, { useContext } from 'react'; import { Grid, Typography, Box } from '@mui/material'; import { makeStyles } from 'tss-react/mui'; import type { Theme } from '@mui/material/styles'; @@ -5,6 +6,7 @@ import MainImage from '../assets/banner-section-main.svg'; import { CTAHeroButton } from './Buttons/Button'; import { useHistory } from 'react-router-dom'; import { useMarketingText } from './../providers/MarketingTextProvider'; +import { ModalContext } from './../providers/ModalProvider'; const useStyles = makeStyles()((theme: Theme) => ({ gridTitle: { @@ -47,8 +49,16 @@ function BannerText() { function BannerSection() { const { banner } = useMarketingText(); const { classes } = useStyles(); - const history = useHistory(); + const modalContext = useContext(ModalContext); + const { openModal } = modalContext; + const handleOpenModal = (modalType: 'SignIn' | 'SignUp') => { + if (modalType === 'SignIn') { + openModal('SignIn'); + } else if (modalType === 'SignUp') { + openModal('SignUp'); + } + }; return ( diff --git a/client/src/components/Buttons/CTAButton.tsx b/client/src/components/Buttons/CTAButton.tsx new file mode 100644 index 00000000..30040466 --- /dev/null +++ b/client/src/components/Buttons/CTAButton.tsx @@ -0,0 +1,45 @@ +import { makeStyles } from 'tss-react/mui'; +import { Typography } from '@mui/material'; +import type { Theme } from '@mui/material/styles'; +import React, { useContext } from 'react'; +import { ModalContext } from '../../providers/ModalProvider'; + +const useStyles = makeStyles()((theme: Theme) => ({ + CTAButton: { + color: 'white', + backgroundColor: '#EF6A60', + borderRadius: '10px', + fontFamily: 'Poppins', + fontWeight: 'semi-bold', + border: 'none', + '&:hover': { + backgroundColor: '#EF6A60', + cursor: 'pointer', + }, + }, +})); + +type Props = { + text: string; +}; + +function CTAButton({ text }: Props) { + const { classes } = useStyles(); + const modalContext = useContext(ModalContext); + const { openModal } = modalContext; + + return ( + + ); +} + +export default CTAButton; diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 1a042941..5cebeadc 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -109,7 +109,7 @@ function Header() { const modalContext = useContext(ModalContext); const { openModal } = modalContext; - const handleoOpenModal = (modalType: 'SignIn' | 'SignUp') => { + const handleOpenModal = (modalType: 'SignIn' | 'SignUp') => { if (modalType === 'SignIn') { openModal('SignIn'); } else if (modalType === 'SignUp') { @@ -423,12 +423,11 @@ function Header() { aria-haspopup="true" aria-expanded={isProfileMenuOpen ? 'true' : undefined} onClick={handleClick} - sx={{ marginRight: '.5em', fill: 'black' }} + sx={{ marginRight: '.5em' }} > ) : ( <> - {/* TODO: Use () => handleoOpenModal('SignUp') when implemented */} history.push('/signup-citizen')} + onClick={() => handleOpenModal('SignUp')} > - diff --git a/client/src/components/Icons/SvgSignUpContactInfoStep.js b/client/src/components/Icons/SvgSignUpContactInfoStep.js new file mode 100644 index 00000000..e1dcc241 --- /dev/null +++ b/client/src/components/Icons/SvgSignUpContactInfoStep.js @@ -0,0 +1,428 @@ +import * as React from 'react'; +const SvgSignUpContactInfoStep = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgSignUpContactInfoStep; diff --git a/client/src/components/Icons/SvgSignUpFinishedStep.js b/client/src/components/Icons/SvgSignUpFinishedStep.js new file mode 100644 index 00000000..b4237964 --- /dev/null +++ b/client/src/components/Icons/SvgSignUpFinishedStep.js @@ -0,0 +1,1475 @@ +import * as React from 'react'; +const SvgSignUpFinishedStep = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgSignUpFinishedStep; diff --git a/client/src/components/Icons/SvgSignUpFocusAreasStep.js b/client/src/components/Icons/SvgSignUpFocusAreasStep.js new file mode 100644 index 00000000..43c0f32c --- /dev/null +++ b/client/src/components/Icons/SvgSignUpFocusAreasStep.js @@ -0,0 +1,1321 @@ +import * as React from 'react'; +const SvgSignUpInterestsStep = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgSignUpInterestsStep; diff --git a/client/src/components/Icons/SvgSignUpLocationStep.js b/client/src/components/Icons/SvgSignUpLocationStep.js new file mode 100644 index 00000000..b3d49eff --- /dev/null +++ b/client/src/components/Icons/SvgSignUpLocationStep.js @@ -0,0 +1,891 @@ +import * as React from 'react'; +const SvgSignUpLocationStep = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgSignUpLocationStep; diff --git a/client/src/components/Icons/SvgSignUpProfileStep.js b/client/src/components/Icons/SvgSignUpProfileStep.js new file mode 100644 index 00000000..f1a82f19 --- /dev/null +++ b/client/src/components/Icons/SvgSignUpProfileStep.js @@ -0,0 +1,1513 @@ +import * as React from 'react'; +const SvgSignUpProfileStep = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgSignUpProfileStep; diff --git a/client/src/components/Modals/SignInModal.tsx b/client/src/components/Modals/SignInModal.tsx index 2607f5b9..d123054e 100644 --- a/client/src/components/Modals/SignInModal.tsx +++ b/client/src/components/Modals/SignInModal.tsx @@ -6,7 +6,8 @@ import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import Dialog from '@mui/material/Dialog'; import DialogContent from '@mui/material/DialogContent'; - +import { useContext } from 'react'; +import { ModalContext } from '../../providers/ModalProvider'; import CloseIcon from '@mui/icons-material/Close'; import IconButton from '@mui/material/IconButton'; @@ -46,13 +47,15 @@ interface Error { type: '' | 'email' | 'password'; message: string; } + const SignInModal = React.forwardRef( ({ closeModal, className }, ref) => { const history = useHistory(); const [isLoading, setIsLoading] = React.useState(false); const [error, setError] = React.useState(null); const { setUser } = React.useContext(UserContext); - + const modalContext = useContext(ModalContext); + const { openModal } = modalContext; const [formData, setFormData] = React.useState(initialFormData); const handleCloseModal = React.useCallback(() => { @@ -174,7 +177,20 @@ const SignInModal = React.forwardRef( Not signed up yet?{' '} - Sign Up + openModal('SignUp')} + > + Sign Up + diff --git a/client/src/components/Modals/SignUpModal.tsx b/client/src/components/Modals/SignUpModal.tsx index 822d2195..b2a67434 100644 --- a/client/src/components/Modals/SignUpModal.tsx +++ b/client/src/components/Modals/SignUpModal.tsx @@ -1,5 +1,16 @@ -// import Button from '@mui/material/Button'; import React from 'react'; +import routes from '../../routes/routes'; +import Grid from '@mui/material/Grid'; +import Dialog from '@mui/material/Dialog'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import CloseIcon from '@mui/icons-material/Close'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import DialogContent from '@mui/material/DialogContent'; +import { Link } from 'react-router-dom'; +import { makeStyles } from 'tss-react/mui'; +import type { Theme } from '@mui/material/styles'; interface SignUpModalProps { closeModal: () => void; @@ -14,19 +25,85 @@ interface SignUpModalProps { }; } +const useStyles = makeStyles()((theme: Theme) => ({ + root: { + border: 0, + maxWidth: '1000px', + maxHeight: '100px', + minWidth: '100px', + minHeight: '100px', + padding: '0px', + }, +})); + const SignUpModal = React.forwardRef( ({ closeModal, className }, ref) => { - // const handleCloseModal = () => { - // closeModal(); - // }; + const handleCloseModal = () => { + closeModal(); + }; + + const { classes } = useStyles(); return ( - //
- //

Sign Up

- // {/* ... SIGNUP CODE ... */} - // - //
-
+
+ + + + + + + + + Welcome Aboard! + + + + + + + + + + + + + + + + + + + +
+
); }, ); diff --git a/client/src/components/Users/Auth/ForgotPassword.tsx b/client/src/components/Users/Auth/ForgotPassword.tsx index 061d5d6b..e4074f10 100644 --- a/client/src/components/Users/Auth/ForgotPassword.tsx +++ b/client/src/components/Users/Auth/ForgotPassword.tsx @@ -68,7 +68,7 @@ function ForgotPassword() { evt.preventDefault(); setIsLoading(true); - const resp = await fetch(`${APP_API_BASE_URL}/auth/reset_password`, { + const resp = await fetch(`${APP_API_BASE_URL}/auth/forgot-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/client/src/components/Users/Auth/SetNewPassword.tsx b/client/src/components/Users/Auth/SetNewPassword.tsx index 0c4ef5e4..f3eaf3e0 100644 --- a/client/src/components/Users/Auth/SetNewPassword.tsx +++ b/client/src/components/Users/Auth/SetNewPassword.tsx @@ -7,10 +7,11 @@ import { makeStyles } from 'tss-react/mui'; import type { Theme } from '@mui/material/styles'; import PasswordInput from './PasswordInput'; -import { UserContext } from '../../../providers'; import { APP_API_BASE_URL } from '../../../configs'; import SetNewPasswordImg from '../../../assets/set-new-password.svg'; +import { useHistory } from 'react-router-dom'; +import { FormHelperText } from '@mui/material'; const useStyles = makeStyles()((theme: Theme) => { const yPadding = 6; @@ -73,16 +74,22 @@ const ERRORS = { }; function SetNewPassword() { - const RESOURCE_URL = `${APP_API_BASE_URL}/auth/users`; + const RESOURCE_URL = `${APP_API_BASE_URL}/auth/reset-password`; const { classes } = useStyles(); const sublabel = `(${PASSWORD_MIN_LENGTH} or more characters)`; const [password, setPassword] = React.useState(''); const [confirmPassword, setConfirmPassword] = React.useState(''); + const [globalError, setGlobalError] = React.useState(''); + const [error, setError] = React.useState(null); const [confirmPasswordError, setConfirmPasswordError] = React.useState(null); + const history = useHistory(); + const searchParams = new URLSearchParams(history.location.search); + const token = searchParams.get('token'); + const handleChange = (evt: React.ChangeEvent): void => { setPassword(evt.target.value); }; @@ -114,8 +121,6 @@ function SetNewPassword() { setConfirmPasswordError(null); }; - const { user } = React.useContext(UserContext); - const handleSubmit = async (evt: React.FormEvent): Promise => { evt.preventDefault(); @@ -129,19 +134,17 @@ function SetNewPassword() { return; } - // ternary in url temporary. User should be logged in(?) id should be available once BE is built - fetch(`${RESOURCE_URL}/${user ? user.id : null}`, { - method: 'PATCH', + fetch(`${RESOURCE_URL}`, { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password }), - credentials: 'include', + body: JSON.stringify({ password, token }), }).then((resp) => { if (resp.ok) { resp.json().then((data) => { // send user to home page/message lets them know password was changed successfully }); } else { - resp.json().then((errors) => setError(errors)); + setGlobalError('Unable to reset password'); } }); }; @@ -175,6 +178,9 @@ function SetNewPassword() { id="confirmPassword" showStartAdornment /> + + {globalError && {globalError}} +
{' '} - - + useEffect(() => { + nextOrNot(activeStep); + }); - - {submitSuccessMessage && } - {submitErrorMessage && } -
+ return ( + + + {(activeStep === 0 && ) || + (activeStep === 1 && ) || + (activeStep === 2 && ) || + (activeStep === 3 && ) || + (activeStep === 4 && ) || + (activeStep === 5 && )} + + + + + {activeStep === 0 && ( - - - ( - - )} - /> - - - ( - <> - - City - - { - if (!errors.ein) { - console.log('add validation here'); - } - }} - error={!!errors.city?.message} - helperText={errors.city?.message ?? ''} - /> - - )} - /> - {orgValidateCityQuery.isLoading ? ( - - ) : ( - <> - {orgValidateCityQuery.isError && ( - {`Invalid EIN ${einApiValidateError}`} - )} - {orgValidateCityQuery.isSuccess && !errors.city && ( - - - - )} - - )} - - - ( - <> - - State - - { - if (!errors.state) { - console.log('add validation here'); - } - }} - error={!!errors.state?.message} - /> - - )} - /> - {orgValidateStateQuery.isLoading ? ( - - ) : ( - <> - {orgValidateStateQuery.isError && ( - {``} - )} - {orgValidateStateQuery.isSuccess && !errors.state && ( - - - - )} - - )} - - - ( - <> - - Employer Identification Number (EIN) - - { - if (!errors.ein) { - setTriggerEinSearch(true); - } - }} - error={!!errors.ein?.message} - helperText={errors.ein?.message ?? ''} - /> - - )} - /> - {orgValidateEinQuery.isLoading ? ( - - ) : ( - <> - {orgValidateEinQuery.isError && ( - {`Invalid EIN ${einApiValidateError}`} - )} - {orgValidateEinQuery.isSuccess && !errors.ein && ( - - - - )} - - )} - - - ( - <> - - IRS Nonprofit Organization Classification - - { - if (!errors.nonprofit_classification) { - console.log('add validation here'); - } - }} - error={!!errors.nonprofit_classification?.message} - helperText={errors.nonprofit_classification?.message ?? ''} - /> - - )} - /> - - + )} - {activeStep === 1 && ( - - - ( - - )} - /> - - - ( - - )} - /> - - - ( - - )} - /> - - - ( - - - - )} - /> - - - ( - - - - )} - /> - - - ( - - )} - /> - - - ( - <> - IRS Nonprofit Organization Classification - - - )} - /> - - {errors.nonprofit_classification - ? errors.nonprofit_classification.message - : ''} - - - + )} - {activeStep === 2 && ( - - - ( - - )} - /> - - - ( - - )} - /> - - - ( - - )} - /> - - - ( - - )} - /> - - - ( - - )} - /> - - - ( - - } - label={ - - } - /> - )} - /> - - - ( - - } - label={} - /> - )} - /> - - + )} - - - - + {activeStep === 3 && ( + + )} + {activeStep === 4 && ( + + )} + {activeStep === 5 && } + + + + + + {(activeStep === 0 || + activeStep === 1 || + activeStep === 2 || + activeStep === 3 || + activeStep === 4) && ( + + )} + {(activeStep === 0 || + activeStep === 1 || + activeStep === 2 || + activeStep === 3 || + activeStep === 4) && ( - - {activeStep === 0 && ( - - )} - {activeStep === 1 && ( - - )} - {activeStep === 2 && ( - - )} - - - - -
- - - + )} + + + + + {isLoading && Loading} + + + + ); -}; +} + +export default SignupNonProfit; diff --git a/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepFive.tsx b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepFive.tsx new file mode 100644 index 00000000..03d54f14 --- /dev/null +++ b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepFive.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Typography from '@mui/material/Typography'; +import { Box } from '@mui/material'; + +type TStepOneProps = { + formData: { + first_name: string; + last_name: string; + email: string; + }; + classes: Record<'header' | 'input' | 'label', string>; +}; + +export default function StepFive({ classes, formData }: TStepOneProps) { + console.log('FINAL FORM BRO', formData); + return ( + + + Sign up almost complete! + + + {formData.first_name} {formData.last_name} + + {formData.email} + + Please check your e-mail to finish the identity verification process. + Afterwards, start contributing! + + + ); +} diff --git a/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepFour.tsx b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepFour.tsx new file mode 100644 index 00000000..fa73e1d1 --- /dev/null +++ b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepFour.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import Grid from '@mui/material/Grid'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import { Box, Avatar, TextField } from '@mui/material'; + +type TStepOneProps = { + formData: { + bio: string; + }; + classes: Record<'header' | 'input' | 'label', string>; + handleChange: (evt: React.ChangeEvent) => void; +}; + +export default function StepFour({ classes, formData, handleChange }: TStepOneProps) { + return ( + + + Finalize your organization's profile. + + + A logo, image, or icon that represents your organization. + + + + + + + + + + + + + A summary about your organization at a glance. + + + + + ); +} diff --git a/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepOne.tsx b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepOne.tsx new file mode 100644 index 00000000..d3049b3d --- /dev/null +++ b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepOne.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import Grid from '@mui/material/Grid'; +import Input from '@mui/material/Input'; +import routes from '../../../../routes/routes'; +import Typography from '@mui/material/Typography'; +import Checkbox from '@mui/material/Checkbox'; +import EmailInput from '../../../Users/Auth/EmailInput'; +import PasswordInput from '../../../Users/Auth/PasswordInput'; +import StyledLink from '../../../StyledLink'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { Box } from '@mui/material'; + +type TStepOneProps = { + formData: { + first_name: string; + last_name: string; + role: string; + email: string; + password: string; + accept_terms?: boolean; + }; + emailError: string; + classes: Record<'header' | 'input', string>; + handleChange: (evt: React.ChangeEvent) => void; +}; + +export default function StepOne({ classes, formData, emailError, handleChange }: TStepOneProps) { + return ( + + + Tell us about yourself. + + Representative Information + + + + + + + + + + + + + + + + + + + + + + + + + } + label={ + + } + /> + + + ); +} diff --git a/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepThree.tsx b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepThree.tsx new file mode 100644 index 00000000..d9aad0de --- /dev/null +++ b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepThree.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import { Box } from '@mui/material'; + +type TStepOneProps = { + formData: { + focusAreas: string[]; + }; + classes: Record<'header' | 'input', string>; + makeChips: () => any; +}; + +export default function StepThree({ classes, formData, makeChips }: TStepOneProps) { + return ( + + + Tell us about your organization's focus area(s). + + + + + What type of work is your organization invovled with? + + + + + {makeChips()} + + + + ); +} diff --git a/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepTwo.tsx b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepTwo.tsx new file mode 100644 index 00000000..07476b67 --- /dev/null +++ b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepTwo.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import Grid from '@mui/material/Grid'; +import Input from '@mui/material/Input'; +import Typography from '@mui/material/Typography'; +import FormControl from '@mui/material/FormControl'; +import { Box } from '@mui/material'; + +type TStepOneProps = { + formData: { + website: string; + facebook: string; + instagram: string; + twitter: string; + }; + classes: Record<'header' | 'input', string>; + handleChange: (evt: React.ChangeEvent) => void; +}; + +export default function StepTwo({ classes, formData, handleChange }: TStepOneProps) { + return ( + + + How can others reach you? + + Contact Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepZero.tsx b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepZero.tsx new file mode 100644 index 00000000..9fa5f063 --- /dev/null +++ b/client/src/components/Users/Auth/SignUpUserAndNonprofit/StepZero.tsx @@ -0,0 +1,196 @@ +import React, { useContext } from 'react'; +import Grid from '@mui/material/Grid'; +import Input from '@mui/material/Input'; +import Typography from '@mui/material/Typography'; +import { ModalContext } from '../../../../providers/ModalProvider'; +import { classifications } from '../../../Users/Auth/SignUpUserAndNonprofit/Classifications'; +import { Box, Select, MenuItem, OutlinedInput, SelectChangeEvent } from '@mui/material'; + +type TStepOneProps = { + formData: { + organization_name: string; + organization_phone: string; + street: string; + city: string; + state: string; + zip_code: string; + employer_identification_number: string; + irs_classification: string; + }; + classes: Record<'header' | 'input', string>; + handleNext: () => void; + handleChange: (evt: React.ChangeEvent) => void; + handleSelectChange: (event: SelectChangeEvent, child: React.ReactNode) => void; + makeStateSelectOptions: () => any; +}; + +export default function StepZero({ + classes, + formData, + handleChange, + handleSelectChange, + makeStateSelectOptions, +}: TStepOneProps) { + const modalContext = useContext(ModalContext); + const { openModal } = modalContext; + + return ( + + + Let's get started! + + + + About Your Organization + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Already have an account? + openModal('SignIn')} + > + Sign In + + + + + + + + ); +} diff --git a/client/src/routes/routes.ts b/client/src/routes/routes.ts index 6d823f34..e61ef9bf 100644 --- a/client/src/routes/routes.ts +++ b/client/src/routes/routes.ts @@ -26,6 +26,7 @@ import SetNewPassword from '../components/Users/Auth/SetNewPassword'; import CookiePolicy from '../views/CookiePolicy'; import TempChat from '../views/TempChat'; import EmailVerification from '../views/EmailVerification'; +import Login from '../views/Login'; type RouteMap = { [componentName: string]: { @@ -91,6 +92,11 @@ const routes: RouteMap = { roles: [], path: '/signup', }, + Login: { + component: Login, + roles: [], + path: '/login', + }, SignupCitizen: { component: SignupCitizen, roles: [], @@ -108,8 +114,8 @@ const routes: RouteMap = { }, User: { component: User, - roles: ['OWNER', 'ADMIN'], - path: '/profile/:id?', + roles: [], + path: '/my-profile', }, ActionForm: { component: ActionForm, diff --git a/client/src/views/FocusAreas.ts b/client/src/views/FocusAreas.ts new file mode 100644 index 00000000..e8d9248f --- /dev/null +++ b/client/src/views/FocusAreas.ts @@ -0,0 +1,19 @@ +export const focusAreas: string[] = [ + 'Animal Care & Services', + 'Poverty', + 'Housing & Homeless', + 'Youth & Children', + 'Disaster Relief', + 'Health Care & Welness', + 'Environment & Sustainability', + 'Sports & Recreation', + 'Seniors', + 'Religion, Faith & Spirituality', + 'Civic Engagement', + 'LGTBQIA+', + 'Civil Rights & Advocacy', + 'Military & Veterans', + 'Social Justice', + 'Education & Literacy', + 'Arts & Culture', +]; diff --git a/client/src/views/Login.tsx b/client/src/views/Login.tsx index 6d50f29c..f33c1210 100644 --- a/client/src/views/Login.tsx +++ b/client/src/views/Login.tsx @@ -1,162 +1,19 @@ -import * as React from 'react'; -import { useHistory } from 'react-router-dom'; -import Button from '@mui/material/Button'; -import Grid from '@mui/material/Grid'; -import Paper from '@mui/material/Paper'; -import { makeStyles } from 'tss-react/mui'; -import Typography from '@mui/material/Typography'; - -import type { Theme } from '@mui/material/styles'; - -import EmailInput from '../components/Users/Auth/EmailInput'; -import FacebookAuthBtn from '../components/Users/Auth/FacebookAuthBtn'; -import GoogleAuthBtn from '../components/Users/Auth/GoogleAuthBtn'; -import PasswordInput from '../components/Users/Auth/PasswordInput'; -import StyledLink from '../components/StyledLink'; -import TextDivider from '../components/TextDivider'; -import { UserContext } from '../providers'; -import routes from '../routes/routes'; -import { APP_API_BASE_URL } from '../configs'; - -const useStyles = makeStyles()((theme: Theme) => { - const xPadding = 12; - const yPadding = 6; - const yMargin = 8; - - return { - paper: { - maxWidth: 821 - theme.spacing(xPadding), - maxHeight: 732 - theme.spacing(yPadding), - borderRadius: 10, - marginTop: theme.spacing(yMargin), - marginBottom: theme.spacing(yMargin), - paddingTop: theme.spacing(yPadding), - paddingBottom: theme.spacing(yPadding), - paddingLeft: theme.spacing(xPadding), - paddingRight: theme.spacing(xPadding), - margin: 'auto', - }, - header: { fontWeight: 'bold', marginBottom: 68 }, - button: { - borderRadius: 0, - height: 62, - textTransform: 'none', - }, - }; -}); - -interface UserLoginData { - email: string; - password: string; -} - -const initialFormData: UserLoginData = { - email: '', - password: '', +import { useContext, useEffect } from 'react'; +import { ModalContext } from '../providers'; + +/** + * This component is navigational. It just serves as a mechanism + * To open the login modal on route navigation + * @returns no content + */ +const Login = () => { + const modalContext = useContext(ModalContext); + + useEffect(() => { + modalContext.openModal('SignIn'); + }, []); + + return ; }; -interface Error { - type: '' | 'email' | 'password'; - message: string; -} - -function Login() { - const { classes } = useStyles(); - const history = useHistory(); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - const { setUser } = React.useContext(UserContext); - - const [formData, setFormData] = React.useState(initialFormData); - - const handleChange = (evt: React.ChangeEvent): void => { - const { name, value }: { name: string; value: string } = evt.target; - setFormData((fData) => ({ - ...fData, - [name]: value, - })); - }; - - const handleSubmit = async (evt: React.FormEvent): Promise => { - evt.preventDefault(); - setIsLoading(true); - const res = await fetch(`${APP_API_BASE_URL}/auth/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData), - credentials: 'include', - }); - const response = await res.json(); - setIsLoading(false); - - if (!res.ok) { - if (response.error === 'Email not found') { - setError({ type: 'email', message: response.error }); - } else if (response.error === 'Invalid password') { - setError({ type: 'password', message: response.error }); - } else { - setError({ type: '', message: 'an unknown error occurred' }); - } - } else { - setUser(response.user, false, true); - setError(null); - history.push('/'); - } - }; - - return ( -
- - - - - Welcome Back. - - - - Sign In with Google - Sign In with Facebook - - - or - - -
- - - - {/* Placeholder for loading - waiting on UI/UX response as to what they want. */} - {isLoading && Loading} - -
- - - Not signed up yet? Sign Up - - -
-
-
- ); -} - export default Login; diff --git a/client/src/views/Main.tsx b/client/src/views/Main.tsx index a00846eb..7629b4f6 100644 --- a/client/src/views/Main.tsx +++ b/client/src/views/Main.tsx @@ -31,6 +31,7 @@ const { Help, TempChat, EmailVerification, + Login, } = routes; function Main() { @@ -52,6 +53,7 @@ function Main() { {/* users */} + diff --git a/client/src/views/Signup.tsx b/client/src/views/Signup.tsx index fb84de60..a21256fc 100644 --- a/client/src/views/Signup.tsx +++ b/client/src/views/Signup.tsx @@ -1,12 +1,19 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; +import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { makeStyles } from 'tss-react/mui'; -import { Grid, Container, Button } from '@mui/material'; - +import { Grid, Button } from '@mui/material'; import type { Theme } from '@mui/material/styles'; - +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; import routes from '../routes/routes'; +// import { Container } from '@mui/material'; keep for future commits +// import StyledLink from '../components/StyledLink'; +// import TextDivider from '../components/TextDivider'; +// import DialogContentText from '@mui/material/DialogContentText'; const useStyles = makeStyles()((theme: Theme) => ({ imageBackground: { @@ -37,39 +44,49 @@ const useStyles = makeStyles()((theme: Theme) => ({ function Signup() { const { classes } = useStyles(); - return ( - - - Welcome! - - - Select your account type. - - - Already have an account? Log In{' '} - + const [open, setOpen] = React.useState(true); + const onClose = (e: any, reason: string) => { + if (reason !== 'backdropClick') { + setOpen(false); + } + }; - - -
- - Are you a non-profit organization? - - - - -
- -
- - Are you an individual citizen? - - - - -
-
-
+ const buttonClose = () => { + setOpen(false); + }; + + return ( +
+ +
+ + Welcome! + + {/* */} + + Account Type + + + + + + + + + + + + + + {/* */} + + + + + +
+
+
); } diff --git a/client/src/views/SignupNonProfit.tsx b/client/src/views/SignupNonProfit.tsx index 3a6e2cd7..2ad3713e 100644 --- a/client/src/views/SignupNonProfit.tsx +++ b/client/src/views/SignupNonProfit.tsx @@ -1,7 +1,7 @@ -import { SignUpUserAndNonprofit } from '../components/Users/Auth/SignUpUserAndNonprofit/SignUpUserAndNonprofit'; +import SignUpUserAndNonprofit from '../components/Users/Auth/SignUpUserAndNonprofit/SignUpUserAndNonprofit'; -function SignupNonProfit() { +function SignUpUserAndNonprofitView() { return ; } -export default SignupNonProfit; +export default SignUpUserAndNonprofitView; diff --git a/client/tsconfig.json b/client/tsconfig.json index 764fd6d9..99bd16be 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -19,13 +15,11 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - + //Material-UI required below: "noImplicitAny": true, "noImplicitThis": true, - "strictNullChecks": true, + "strictNullChecks": true }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/server/sample.env b/server/sample.env index 90e47caf..006ab874 100644 --- a/server/sample.env +++ b/server/sample.env @@ -27,3 +27,6 @@ AZURE_BLOB_CONTAINER=images JWT_SECRET=foobar SENDGRID_API_KEY=SG.foobar FE_DOMAIN=http://localhost:3000 + +# Cookie +COOKIE_DOMAIN=localhost diff --git a/server/src/acccount-manager/account-manager.controller.ts b/server/src/acccount-manager/account-manager.controller.ts index d6b8d9f1..919a1e8c 100644 --- a/server/src/acccount-manager/account-manager.controller.ts +++ b/server/src/acccount-manager/account-manager.controller.ts @@ -17,6 +17,7 @@ import { InternalServerErrorException, HttpException, HttpStatus, + UnauthorizedException, } from '@nestjs/common'; import { ApiTags, ApiBody, ApiConsumes, ApiOperation, ApiExcludeEndpoint } from '@nestjs/swagger'; import { JwtService } from '@nestjs/jwt'; @@ -34,7 +35,7 @@ import { UsersService } from './user.service'; import { FileInterceptor } from '@nestjs/platform-express'; import { FileSizes } from '../file-storage/domain'; import { FilesStorageService } from '../file-storage/file-storage.service'; -import { VerifyEmailDto, ReturnSessionDto, ReturnUserDto } from './dto/auth.dto'; +import { VerifyEmailDto, ReturnSessionDto, ReturnUserDto, ResetPasswordDto } from './dto/auth.dto'; import { UpdateUserInternal } from './dto/create-user.internal'; import { MapTo } from '../shared/serialize.interceptor'; @@ -114,6 +115,10 @@ export class AccountManagerController { @Response({ passthrough: true }) response: ResponseT, ): Promise { const { user } = request; + if (!user) { + throw new UnauthorizedException(); + } + // TODO: we probably need a better solution for this if (!user.email_verified && process.env.NODE_ENV === 'staging') { throw new HttpException( @@ -125,12 +130,12 @@ export class AccountManagerController { const jwt = await this.accountManagerService.createJwt(user); response .cookie(COOKIE_KEY, jwt, { - domain: 'localhost', + domain: process.env.COOKIE_DOMAIN ?? 'localhost', expires: new Date(new Date().getTime() + 60 * 60 * 1000), // 1 hour httpOnly: true, path: '/', sameSite: 'strict', - secure: process.env.NODE_ENV === 'production', + secure: process.env.NODE_ENV !== 'development', signed: true, }) .send({ user }); @@ -150,12 +155,20 @@ export class AccountManagerController { @Get('logout') @ApiOperation({ summary: 'Logout' }) logout(@Response({ passthrough: true }) response: ResponseT): void { - response.clearCookie(COOKIE_KEY).send(); + response + .clearCookie(COOKIE_KEY, { + domain: process.env.COOKIE_DOMAIN ?? 'localhost', + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: process.env.NODE_ENV !== 'development', + }) + .send(); } - @Post('reset_password') - @ApiOperation({ summary: 'Password reset' }) - async resetPassword( + @Post('forgot-password') + @ApiOperation({ summary: 'Password reset Request' }) + async resetPasswordRequest( @Request() req, @Response({ passthrough: true }) response: ResponseT, ): Promise { @@ -167,7 +180,7 @@ export class AccountManagerController { } const jwt = await this.jwtService.sign( - { valid: true }, + { valid: true, id: user.id }, { expiresIn: '1h', secret: process.env.JWT_SECRET }, ); @@ -183,8 +196,9 @@ export class AccountManagerController {

The Givingful Team

`, }; - - await this.sendgridService.send(mail); + if (process.env.NODE_ENV === 'staging') { + await this.sendgridService.send(mail); + } response.status(200); } catch (e) { // always respond 200 so hackerz don't know which emails are active and not @@ -192,6 +206,23 @@ export class AccountManagerController { } } + @Put('reset-password') + async resetPassword( + @Body() resetPasswordDtO: ResetPasswordDto, + ): Promise { + try { + const { id } = await this.jwtService.verify(resetPasswordDtO.token, { + secret: process.env.JWT_SECRET, + }); + if (id) { + this.usersService.updatePasswod(id, { password: resetPasswordDtO.password }); + } + return true; + } catch { + throw new BadRequestException('jwt verify fail'); + } + } + @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Upload profile image' }) @ApiBody({ diff --git a/server/src/acccount-manager/account-manager.service.ts b/server/src/acccount-manager/account-manager.service.ts index c986f08d..8d85c43e 100644 --- a/server/src/acccount-manager/account-manager.service.ts +++ b/server/src/acccount-manager/account-manager.service.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcryptjs'; import { User } from './entities/user.entity'; @@ -24,12 +24,11 @@ export class AccountManagerService { delete user.password; return user; } else { - throw new Error(); + Logger.error(`Error validating login for user ${email}`, AccountManagerService.name); + throw new Error('Error Validating user login'); } } catch (err) { - err.status = HttpStatus.UNAUTHORIZED; - err.response.status = HttpStatus.UNAUTHORIZED; - throw err; + return null; } } diff --git a/server/src/acccount-manager/dto/auth.dto.ts b/server/src/acccount-manager/dto/auth.dto.ts index a207149f..9800717d 100644 --- a/server/src/acccount-manager/dto/auth.dto.ts +++ b/server/src/acccount-manager/dto/auth.dto.ts @@ -13,11 +13,21 @@ export class LoginDto { password: string; } -export class ResetPasswordDto { +export class ResetPasswordRequestDto { @IsEmail() email: string; } +export class ResetPasswordDto { + @IsNotEmpty() + @IsString() + password: string; + + @IsNotEmpty() + @IsString() + token: string; +} + export class ReturnUserDto { @Expose() id: number; diff --git a/server/src/acccount-manager/user.service.ts b/server/src/acccount-manager/user.service.ts index 3582efc0..24c4980a 100644 --- a/server/src/acccount-manager/user.service.ts +++ b/server/src/acccount-manager/user.service.ts @@ -26,6 +26,20 @@ export class UsersService { } } + async updatePasswod(id: number, createUserInternal: Partial): Promise { + try { + const hashedPw = await bcrypt.hash(createUserInternal.password, parseInt(BCRYPT_WORK_FACTOR)); + await this.usersRepository.update(id, { password: hashedPw }); + const user = await this.usersRepository.findOneBy({ id }); + delete user.password; + Logger.log(`Updating password for user: ${id}`); + return user; + } catch (err: any) { + Logger.error(`${err.message}: \n${err.stack}`, UsersService.name); + throw new Error(`Error updating user password for user ${id}`); + } + } + async findOne(id: number): Promise> { const user = await this.usersRepository.findOneBy({ id }); if (user) { diff --git a/server/src/database-connection.service.ts b/server/src/database-connection.service.ts index b8bc06ba..c8d962fc 100644 --- a/server/src/database-connection.service.ts +++ b/server/src/database-connection.service.ts @@ -36,12 +36,7 @@ const appConfigs = (environment: AppEnvironment): TypeOrmModuleOptions => { case 'staging': return { ...defaultOptions, - ssl: { - ca: process.env.POSTGRESQL_SSL_CA ?? '', - cert: process.env.POSTGRESQL_SSL_CERT ?? '', - key: process.env.POSTGRESQL_SSL_KEY ?? '', - rejectUnauthorized: false, - }, + ssl: true, }; case 'development': diff --git a/server/src/file-storage/file-storage.service.ts b/server/src/file-storage/file-storage.service.ts index 76109c99..b254e797 100644 --- a/server/src/file-storage/file-storage.service.ts +++ b/server/src/file-storage/file-storage.service.ts @@ -40,10 +40,10 @@ export class FilesStorageService { * @returns the stored file path */ public async storeImage(storageArgs: FileStorageArgs): Promise { - if (process.env.NODE_ENV === 'production') { - return await this.azureStorage.uploadFile(storageArgs); - } else { + if (process.env.NODE_ENV === 'development') { return await this.diskStorage.uploadFile(storageArgs); + } else { + return await this.azureStorage.uploadFile(storageArgs); } } }