From 5b8b1414cb0a7cc8d27ea02b9e9dbede287d6705 Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Thu, 20 Mar 2025 16:27:20 -0400 Subject: [PATCH 1/7] add api call --- .../src/pages/registerPage/formPage.tsx | 111 +++++++++++++----- 1 file changed, 83 insertions(+), 28 deletions(-) diff --git a/apps/frontend/src/pages/registerPage/formPage.tsx b/apps/frontend/src/pages/registerPage/formPage.tsx index 59793fa1..9dc2bb7a 100644 --- a/apps/frontend/src/pages/registerPage/formPage.tsx +++ b/apps/frontend/src/pages/registerPage/formPage.tsx @@ -11,14 +11,28 @@ import { useState } from 'react'; import InfoIcon from '@mui/icons-material/Info'; import { Visibility, VisibilityOff } from '@mui/icons-material'; +import {UserModel, NewUserInput } from '../../types/UserModel'; +import {SignUpDto} from '../../types/SignUpModel'; + interface FormPageProps { setIsSubmitted: (value: boolean) => void; } + + + +const api = 'http://localhost:3000/'; + const FormPage: React.FC = ({ setIsSubmitted }) => { const [showPassword, setShowPassword] = useState(false); const [showRePassword, setShowRePassword] = useState(false); + // States for the form fields + const [userId, setUserId] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [rePassword, setRePassword] = useState(''); + const handleTogglePassword = () => { setShowPassword((prev) => !prev); }; @@ -27,6 +41,41 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { setShowRePassword((prev) => !prev); }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + const matchingUser = await fetch(api + "users/" + userId); + if (!matchingUser.ok) { + throw new Error(matchingUser.statusText); + } + const user: UserModel = await matchingUser.json(); + + if (user.email !== email) { + alert('User ID and email do not match. Please try again.'); + return; + } + + const inputUser: SignUpDto = { email, password }; + + const response = await fetch(api + 'auth/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(inputUser), + }); + + if (response.ok) { + alert('Account created successfully!'); + setIsSubmitted(true); + } else { + alert('There was an error. Please try again.'); + } + } catch (error) { + console.error("Error during form submission:", error); + alert("An error occurred. Please check your inputs and try again."); + } + }; + return ( <>

= ({ setIsSubmitted }) => { > Volunteer Registration -

- + + = ({ setIsSubmitted }) => { setUserId(e.target.value)} color="success" variant="outlined" fullWidth @@ -103,6 +151,9 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { Email setEmail(e.target.value)} color="success" variant="outlined" fullWidth @@ -126,7 +177,10 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { Password setPassword(e.target.value)} color="success" variant="outlined" fullWidth @@ -138,7 +192,7 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { InputProps={{ endAdornment: ( - + {showPassword ? : } @@ -159,7 +213,10 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { Re-enter Password setRePassword(e.target.value)} color="success" variant="outlined" fullWidth @@ -171,7 +228,7 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { InputProps={{ endAdornment: ( - + {showRePassword ? : } @@ -181,27 +238,25 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { '& .MuiFilledInput-root': { fontFamily: 'Montserrat' }, }} /> + - ); }; From ee3f432d546f5d139adccce70e7ae89062b3591b Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Thu, 20 Mar 2025 16:27:30 -0400 Subject: [PATCH 2/7] add dto --- apps/frontend/src/types/SignUpModel.ts | 4 ++++ apps/frontend/src/types/UserModel.ts | 33 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 apps/frontend/src/types/SignUpModel.ts create mode 100644 apps/frontend/src/types/UserModel.ts diff --git a/apps/frontend/src/types/SignUpModel.ts b/apps/frontend/src/types/SignUpModel.ts new file mode 100644 index 00000000..161ed54a --- /dev/null +++ b/apps/frontend/src/types/SignUpModel.ts @@ -0,0 +1,4 @@ +export type SignUpDto = { + email: string; + password: string; +} diff --git a/apps/frontend/src/types/UserModel.ts b/apps/frontend/src/types/UserModel.ts new file mode 100644 index 00000000..a018fae7 --- /dev/null +++ b/apps/frontend/src/types/UserModel.ts @@ -0,0 +1,33 @@ +export type UserModel = { + userId: number, + firstName: string, + lastName: string, + phoneNumber: number, + email: string, + siteIds: number[], + zipCode: number, + birthDate: Date, + role: Role, + status: UserStatus +}; + + +export enum Role { + VOLUNTEER = "Volunteer", + ADMIN = "Admin", +}; + +export enum UserStatus { + APPROVED = "Approved", + PENDING = "Pending", + DENIED = "Denied", +}; + +export type NewUserInput = { + firstName: string, + lastName: string, + phoneNumber: string, + email: string, + zipCode: string, + birthDate: string, +}; \ No newline at end of file From be502bbc20d2868061da2e99c4e47f5f11ba4f67 Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Thu, 10 Apr 2025 18:17:39 -0400 Subject: [PATCH 3/7] improve error handling to debug login --- apps/backend/src/auth/auth.service.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index ccc4b6d4..4994cac5 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -60,8 +60,14 @@ export class AuthService { Password: password, }); - const response = await this.providerClient.send(signUpCommand); - return response.UserConfirmed; + try { + const response = await this.providerClient.send(signUpCommand); + return response.UserConfirmed; + } catch (e) { + console.log(e) + throw new Error(e.message); + } + } async signin({ email, password }: SignInDto): Promise { From 5bf7473ec7d598403ea87c356e64b45809d3bc35 Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Thu, 10 Apr 2025 18:28:44 -0400 Subject: [PATCH 4/7] switch to axios --- apps/backend/src/dtos/sign-up.dto.ts | 3 ++ .../src/pages/registerPage/formPage.tsx | 48 ++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/apps/backend/src/dtos/sign-up.dto.ts b/apps/backend/src/dtos/sign-up.dto.ts index a900249a..03d99463 100644 --- a/apps/backend/src/dtos/sign-up.dto.ts +++ b/apps/backend/src/dtos/sign-up.dto.ts @@ -1,9 +1,12 @@ import { IsEmail, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class SignUpDto { + @ApiProperty({description: "The user's email address", example: "test@gmail.com"}) @IsEmail() email: string; + @ApiProperty({description: "The user's password", example: "password123"}) @IsString() password: string; } diff --git a/apps/frontend/src/pages/registerPage/formPage.tsx b/apps/frontend/src/pages/registerPage/formPage.tsx index 9dc2bb7a..5f34d6e9 100644 --- a/apps/frontend/src/pages/registerPage/formPage.tsx +++ b/apps/frontend/src/pages/registerPage/formPage.tsx @@ -13,6 +13,7 @@ import { Visibility, VisibilityOff } from '@mui/icons-material'; import {UserModel, NewUserInput } from '../../types/UserModel'; import {SignUpDto} from '../../types/SignUpModel'; +import axios from 'axios'; interface FormPageProps { setIsSubmitted: (value: boolean) => void; @@ -20,9 +21,6 @@ interface FormPageProps { - -const api = 'http://localhost:3000/'; - const FormPage: React.FC = ({ setIsSubmitted }) => { const [showPassword, setShowPassword] = useState(false); const [showRePassword, setShowRePassword] = useState(false); @@ -45,34 +43,38 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { e.preventDefault(); try { - const matchingUser = await fetch(api + "users/" + userId); - if (!matchingUser.ok) { - throw new Error(matchingUser.statusText); - } - const user: UserModel = await matchingUser.json(); - + const matchingResponse = await axios.get(`${process.env.VITE_API_BASE_URL}/users/${userId}`); + const user: UserModel = matchingResponse.data; + // continue using `user` here + if (user.email !== email) { alert('User ID and email do not match. Please try again.'); return; } + + if (password !== rePassword) { + alert('Passwords do not match. Please try again.'); + return; + } const inputUser: SignUpDto = { email, password }; - - const response = await fetch(api + 'auth/signup', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(inputUser), - }); - - if (response.ok) { - alert('Account created successfully!'); - setIsSubmitted(true); - } else { - alert('There was an error. Please try again.'); + + try { + const response = await axios.post(`${process.env.VITE_API_BASE_URL}/auth/signup`, inputUser); + } catch (error) { + if (axios.isAxiosError(error)) { + console.error('Error fetching user:', error.response?.status, error.message); + } else { + console.error('Unexpected error:', error); + } } + } catch (error) { - console.error("Error during form submission:", error); - alert("An error occurred. Please check your inputs and try again."); + if (axios.isAxiosError(error)) { + console.error('Error fetching user:', error.response?.status, error.message); + } else { + console.error('Unexpected error:', error); + } } }; From 43d30ef39800a2084a7b0d9028e283169c98e779 Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Sun, 13 Apr 2025 15:26:14 -0400 Subject: [PATCH 5/7] switch to axios and use env variable --- apps/frontend/src/pages/registerPage/formPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/pages/registerPage/formPage.tsx b/apps/frontend/src/pages/registerPage/formPage.tsx index 5f34d6e9..261dbbc3 100644 --- a/apps/frontend/src/pages/registerPage/formPage.tsx +++ b/apps/frontend/src/pages/registerPage/formPage.tsx @@ -43,7 +43,7 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { e.preventDefault(); try { - const matchingResponse = await axios.get(`${process.env.VITE_API_BASE_URL}/users/${userId}`); + const matchingResponse = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/users/${userId}`); const user: UserModel = matchingResponse.data; // continue using `user` here @@ -60,7 +60,8 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { const inputUser: SignUpDto = { email, password }; try { - const response = await axios.post(`${process.env.VITE_API_BASE_URL}/auth/signup`, inputUser); + const response = await axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/signup`, inputUser); + setIsSubmitted(true); } catch (error) { if (axios.isAxiosError(error)) { console.error('Error fetching user:', error.response?.status, error.message); From 4661c3b0c7b76821ed37a84c04db10d36eef8056 Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Tue, 15 Apr 2025 18:11:10 -0400 Subject: [PATCH 6/7] Add better alerts and switch to formik (resolve conflicts) --- .../src/pages/registerPage/formPage.tsx | 238 +++++++++++++----- 1 file changed, 169 insertions(+), 69 deletions(-) diff --git a/apps/frontend/src/pages/registerPage/formPage.tsx b/apps/frontend/src/pages/registerPage/formPage.tsx index 261dbbc3..c707ed85 100644 --- a/apps/frontend/src/pages/registerPage/formPage.tsx +++ b/apps/frontend/src/pages/registerPage/formPage.tsx @@ -6,31 +6,39 @@ import { Tooltip, InputAdornment, IconButton, + FormHelperText, } from '@mui/material'; import { useState } from 'react'; import InfoIcon from '@mui/icons-material/Info'; import { Visibility, VisibilityOff } from '@mui/icons-material'; +import { Snackbar, Alert } from '@mui/material'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; -import {UserModel, NewUserInput } from '../../types/UserModel'; -import {SignUpDto} from '../../types/SignUpModel'; +import { UserModel, NewUserInput } from '../../types/UserModel'; +import { SignUpDto } from '../../types/SignUpModel'; import axios from 'axios'; + + + interface FormPageProps { setIsSubmitted: (value: boolean) => void; } - - const FormPage: React.FC = ({ setIsSubmitted }) => { + const [snackbar, setSnackbar] = useState<{ + open: boolean; + message: string; + severity: 'error' | 'info' | 'success' | 'warning'; // 👈 enforce correct type + }>({ + open: false, + message: '', + severity: 'info', + }); const [showPassword, setShowPassword] = useState(false); const [showRePassword, setShowRePassword] = useState(false); - // States for the form fields - const [userId, setUserId] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [rePassword, setRePassword] = useState(''); - const handleTogglePassword = () => { setShowPassword((prev) => !prev); }; @@ -39,45 +47,63 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { setShowRePassword((prev) => !prev); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - try { - const matchingResponse = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/users/${userId}`); - const user: UserModel = matchingResponse.data; - // continue using `user` here - - if (user.email !== email) { - alert('User ID and email do not match. Please try again.'); - return; - } - - if (password !== rePassword) { - alert('Passwords do not match. Please try again.'); - return; - } - - const inputUser: SignUpDto = { email, password }; + // Define the validation schema using Yup + const validationSchema = Yup.object({ + userId: Yup.string().required('User ID is required'), + email: Yup.string() + .email('Enter a valid email address') + .required('Email is required'), + password: Yup.string().required('Password is required'), + rePassword: Yup.string() + .required('Please confirm your password') + .oneOf([Yup.ref('password')], 'Passwords must match'), + }); + // Initialize formik + const formik = useFormik({ + initialValues: { + userId: '', + email: '', + password: '', + rePassword: '', + }, + validationSchema, + onSubmit: async (values) => { try { - const response = await axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/signup`, inputUser); - setIsSubmitted(true); + + const matchingResponse = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/users/${values.userId}`); + const user: UserModel = matchingResponse.data; + if (user.email !== values.email) { + console.log('User ID and email do not match'); + setSnackbar({ open: true, message: 'User ID and email do not match', severity: 'error' }); + return; + } + + if (values.password !== values.rePassword) { + setSnackbar({ open: true, message: 'Passwords do not match. Please try again.', severity: 'error' }); + return; + } + + const inputUser: SignUpDto = { email: values.email, password: values.password }; + + await axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/signup`, inputUser); } catch (error) { if (axios.isAxiosError(error)) { - console.error('Error fetching user:', error.response?.status, error.message); + const msg = error.response?.data.message || ''; + if (msg.includes("Password did not conform with policy: ")) { + setSnackbar({ open: true, message: msg.split(': ')[1], severity: 'error' }); + } else { + console.error('Unhandled Axios error:', error); + } } else { - console.error('Unexpected error:', error); + console.error('Unhandled error:', error); } } + }, + }); - } catch (error) { - if (axios.isAxiosError(error)) { - console.error('Error fetching user:', error.response?.status, error.message); - } else { - console.error('Unexpected error:', error); - } - } - }; + // Check if the form is valid and all fields have been touched + const isFormValid = formik.isValid && Object.keys(formik.touched).length === 4; return ( <> @@ -102,7 +128,7 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { > Volunteer Registration -
+ = ({ setIsSubmitted }) => { setUserId(e.target.value)} + value={formik.values.userId} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={formik.touched.userId && Boolean(formik.errors.userId)} color="success" variant="outlined" fullWidth @@ -139,24 +167,39 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { InputLabelProps={{ style: { fontFamily: 'Montserrat', fontWeight: 600 }, }} - sx={{ - '& .MuiFilledInput-root': { fontFamily: 'Montserrat' }, - }} + sx={{ '& .MuiFilledInput-root': { fontFamily: 'Montserrat' } }} /> + {formik.touched.userId && formik.errors.userId && ( + + {formik.errors.userId} + + )} + Email setEmail(e.target.value)} + value={formik.values.email} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={formik.touched.email && Boolean(formik.errors.email)} color="success" variant="outlined" fullWidth @@ -165,25 +208,40 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { InputLabelProps={{ style: { fontFamily: 'Montserrat', fontWeight: 600 }, }} - sx={{ - '& .MuiFilledInput-root': { fontFamily: 'Montserrat' }, - }} + sx={{ '& .MuiFilledInput-root': { fontFamily: 'Montserrat' } }} /> + {formik.touched.email && formik.errors.email && ( + + {formik.errors.email} + + )} + Password setPassword(e.target.value)} + value={formik.values.password} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={formik.touched.password && Boolean(formik.errors.password)} color="success" variant="outlined" fullWidth @@ -201,25 +259,40 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { ), }} - sx={{ - '& .MuiFilledInput-root': { fontFamily: 'Montserrat' }, - }} + sx={{ '& .MuiFilledInput-root': { fontFamily: 'Montserrat' } }} /> + {formik.touched.password && formik.errors.password && ( + + {formik.errors.password} + + )} + Re-enter Password setRePassword(e.target.value)} + value={formik.values.rePassword} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={formik.touched.rePassword && Boolean(formik.errors.rePassword)} color="success" variant="outlined" fullWidth @@ -237,13 +310,27 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { ), }} - sx={{ - '& .MuiFilledInput-root': { fontFamily: 'Montserrat' }, - }} + sx={{ '& .MuiFilledInput-root': { fontFamily: 'Montserrat' } }} /> + {formik.touched.rePassword && formik.errors.rePassword && ( + + {formik.errors.rePassword} + + )} + + setSnackbar({ ...snackbar, open: false })} + > + setSnackbar({ ...snackbar, open: false })}> + {snackbar.message} + + ); }; From fcf687ff9049094771833074bcffcdd5f04991ab Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Tue, 15 Apr 2025 18:15:36 -0400 Subject: [PATCH 7/7] fix submission and add user exists alert --- apps/frontend/src/pages/registerPage/formPage.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/pages/registerPage/formPage.tsx b/apps/frontend/src/pages/registerPage/formPage.tsx index c707ed85..7be37eb3 100644 --- a/apps/frontend/src/pages/registerPage/formPage.tsx +++ b/apps/frontend/src/pages/registerPage/formPage.tsx @@ -87,16 +87,17 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { const inputUser: SignUpDto = { email: values.email, password: values.password }; await axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/signup`, inputUser); + setIsSubmitted(true); } catch (error) { if (axios.isAxiosError(error)) { const msg = error.response?.data.message || ''; if (msg.includes("Password did not conform with policy: ")) { setSnackbar({ open: true, message: msg.split(': ')[1], severity: 'error' }); - } else { - console.error('Unhandled Axios error:', error); - } + } else if (msg.includes("User already exists")) { + setSnackbar({ open: true, message: 'User already exists', severity: 'error' }); + } } else { - console.error('Unhandled error:', error); + setSnackbar({ open: true, message: 'An unexpected error occurred', severity: 'error' }); } } },