diff --git a/README.md b/README.md index a91c72c1..4e7b2579 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The RDS App is a secure, accessible, and open-source web application that stream | Hosting | Azure Web Service | | QR Scanning | Html5QrcodeScanner, QRCodeCanvas | -## Directory +## Directory (old) ```plaintext client/ # Client-facing React application @@ -196,24 +196,24 @@ The items listed below are features our team has identified out of scope for the **App Features** -- Auto-populate location using GPS location coordinates -- Widget for staff to comment on survey responses -- Integration with Homeless Management Information System (HMIS) database system -- Volunteer scheduling dashboard for administrators -- Automated SMS gift card distribution -- Resume unfinished survey feature -- Admin ability to edit survey questions -- Volunteer ability to edit survey responses -- Survey analytics dashboard +- Auto-populate location using GPS location coordinates +- Widget for staff to comment on survey responses +- Integration with Homeless Management Information System (HMIS) database system +- Volunteer scheduling dashboard for administrators +- Automated SMS gift card distribution +- Resume unfinished survey feature +- Admin ability to edit survey questions +- Volunteer ability to edit survey responses +- Survey analytics dashboard **Testing** -- Dynamic Application Security Testing (DAST) +- Dynamic Application Security Testing (DAST) **User Experience** -Step-by-step user training guide -- Setup wizard +- Setup wizard ## Contributors diff --git a/client/src/contexts/AuthContext.tsx b/client/src/contexts/AuthContext.tsx index 928a352e..58e0179d 100644 --- a/client/src/contexts/AuthContext.tsx +++ b/client/src/contexts/AuthContext.tsx @@ -16,8 +16,12 @@ import { interface AuthState { token: string; firstName: string; + lastName: string; userObjectId: string; userRole: string; + email: string; + phone: string; + locationObjectId: string; lastestLocationObjectId: string; permissions: { action: Action; @@ -32,7 +36,11 @@ interface AuthContextValue extends AuthState { setToken: (token: string) => void; setUserObjectId: (userObjectId: string) => void; setFirstName: (firstName: string) => void; + setLastName: (lastName: string) => void; setUserRole: (userRole: string) => void; + setEmail: (email: string) => void; + setPhone: (phone: string) => void; + setLocationObjectId: (locationObjectId: string) => void; setLastestLocationObjectId: (lastestLocationObjectId: string) => void; setPermissions: ( permissions: { @@ -51,8 +59,12 @@ function getDefaultAuthState(): AuthState { return { token: '', firstName: '', + lastName: '', userObjectId: '', userRole: '', + email: '', + phone: '', + locationObjectId: '', lastestLocationObjectId: '', permissions: [], isLoggedIn: false, @@ -70,10 +82,14 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { return { token, - firstName: decoded.firstName, - userRole: decoded.role, + firstName: '', + lastName: '', + email: '', + phone: '', + userRole: '', userObjectId: decoded.userObjectId, permissions: [], + locationObjectId: '', lastestLocationObjectId: '', isLoggedIn: true, // TODO: Verification needed for consistency. @@ -129,7 +145,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { ...prev, userObjectId: user.data._id, firstName: user.data.firstName, + lastName: user.data.lastName, userRole: user.data.role, + email: user.data.email, + phone: user.data.phone, + locationObjectId: user.data.locationObjectId, lastestLocationObjectId: latestLocationObjectId, permissions: user.data.permissions, isLoggedIn: true, @@ -153,10 +173,26 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { setState(prev => ({ ...prev, firstName })); }; + const setLastName = (lastName: string) => { + setState(prev => ({ ...prev, lastName })); + }; + const setUserRole = (userRole: string) => { setState(prev => ({ ...prev, userRole })); }; + const setEmail = (email: string) => { + setState(prev => ({ ...prev, email })); + }; + + const setPhone = (phone: string) => { + setState(prev => ({ ...prev, phone })); + }; + + const setLocationObjectId = (locationObjectId: string) => { + setState(prev => ({ ...prev, locationObjectId })); + }; + const setLastestLocationObjectId = (lastestLocationObjectId: string) => { setState(prev => ({ ...prev, lastestLocationObjectId })); }; @@ -176,7 +212,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { setToken, setUserObjectId, setFirstName, + setLastName, setUserRole, + setEmail, + setPhone, + setLocationObjectId, setLastestLocationObjectId, setPermissions, clearSession, diff --git a/client/src/hooks/useAuth.tsx b/client/src/hooks/useAuth.tsx index b1cb1cbb..2b6a3c7d 100644 --- a/client/src/hooks/useAuth.tsx +++ b/client/src/hooks/useAuth.tsx @@ -3,7 +3,7 @@ import { useAuthContext } from '@/contexts'; import { useSurveyStore } from '@/stores/useSurveyStore'; import { deleteAuthToken, - getObjectId, + getDecodedAuthToken, saveAuthToken } from '@/utils/authTokenHandler'; @@ -18,8 +18,10 @@ export const useAuth = () => { const handleLogin = async (token: string) => { saveAuthToken(token); - const userObjectId = getObjectId(); - await fetchUserContext(userObjectId); + const userObjectId = getDecodedAuthToken()?.userObjectId; + if (userObjectId) { + await fetchUserContext(userObjectId); + } }; const handleLogout = () => { diff --git a/client/src/pages/Login/Login.tsx b/client/src/pages/Login/Login.tsx index 8869176f..20d82713 100644 --- a/client/src/pages/Login/Login.tsx +++ b/client/src/pages/Login/Login.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import { initializeSurveyStore } from '@/utils/authTokenHandler'; +import { useSurveyStore } from '@/stores'; import { Box, Button, Paper, TextField, Typography } from '@mui/material'; import { useNavigate } from 'react-router-dom'; @@ -9,6 +9,7 @@ import { useAuth } from '@/hooks/useAuth'; export default function Login() { const { handleLogin } = useAuth(); const navigate = useNavigate(); + const { clearSession } = useSurveyStore(); // const [email, setEmail] = useState(''); const [phone, setPhone] = useState(''); const [otp, setOtp] = useState(''); @@ -83,7 +84,7 @@ export default function Login() { if (response.ok) { // Successful login and store user data await handleLogin(data.token); - initializeSurveyStore(); + clearSession(); // Clear any previous survey data from storage navigate(data.redirectTo); } else { setErrorMessage(data.message); diff --git a/client/src/pages/Profile/Profile.tsx b/client/src/pages/Profile/Profile.tsx index 6a0fd977..97a68a48 100644 --- a/client/src/pages/Profile/Profile.tsx +++ b/client/src/pages/Profile/Profile.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; +import { useAuthContext } from '@/contexts'; import { useAbility, useApi } from '@/hooks'; import { ACTIONS, SUBJECTS } from '@/permissions/constants'; import { subject } from '@casl/ability'; @@ -13,36 +14,43 @@ import { RoleSelect } from '@/components/forms'; -export default function AdminEditProfile() { +export default function Profile() { const ability = useAbility(); const { userService } = useApi(); const { id } = useParams(); const [message, setMessage] = useState(''); const [error, _setError] = useState(''); - - // REVIEW: Let's use auth context, such that you first refresh the auth context for this user, and then fetch the user data from the context. - const { data: user, mutate } = userService.useUser(id) ?? {}; + const { + locationObjectId, + firstName, + lastName, + userObjectId, + userRole, + email, + phone, + setEmail, + setPhone, + setLocationObjectId, + setUserRole + } = useAuthContext(); const canEditField = (field: string) => { - if (!user) return false; // wait until user is fetched return ability.can( ACTIONS.CASL.UPDATE, - subject(SUBJECTS.USER, user), + subject(SUBJECTS.USER, { _id: userObjectId }), field ); }; const handleSave = async () => { - if (!user) return; - try { const updatePayload: Record = {}; - if (canEditField('role')) updatePayload.role = user.role; - if (canEditField('email')) updatePayload.email = user.email; - if (canEditField('phone')) updatePayload.phone = user.phone; + if (canEditField('role')) updatePayload.role = userRole; + if (canEditField('email')) updatePayload.email = email; + if (canEditField('phone')) updatePayload.phone = phone; if (canEditField('locationObjectId')) - updatePayload.locationObjectId = user.locationObjectId; + updatePayload.locationObjectId = locationObjectId; const updatedUser = await userService.updateUser( id!, @@ -53,7 +61,6 @@ export default function AdminEditProfile() { setMessage( updatedUser.message || 'Profile updated successfully!' ); - mutate?.(); // refetch the user } else { setMessage(updatedUser.message || 'Failed to update profile.'); } @@ -70,7 +77,20 @@ export default function AdminEditProfile() { } } - mutate?.({ ...user, [field]: value }, false); + switch (field) { + case 'email': + setEmail(value); + break; + case 'phone': + setPhone(value); + break; + case 'locationObjectId': + setLocationObjectId(value); + break; + case 'role': + setUserRole(value); + break; + } }; return ( @@ -86,8 +106,7 @@ export default function AdminEditProfile() { }} > - Profile for{' '} - {`${user?.firstName || 'User'} ${user?.lastName || ''}`} + Profile for {`${firstName || 'User'} ${lastName || ''}`} {error ? ( @@ -98,7 +117,7 @@ export default function AdminEditProfile() { handleChange('email', e.target.value) } @@ -108,7 +127,7 @@ export default function AdminEditProfile() { handleChange('phone', e.target.value) } @@ -117,7 +136,7 @@ export default function AdminEditProfile() { /> handleChange('role', e.target.value as string) } @@ -126,7 +145,7 @@ export default function AdminEditProfile() { /> handleChange( 'locationObjectId', diff --git a/client/src/stores/useSurveyStore.tsx b/client/src/stores/useSurveyStore.tsx index 5f882aee..4a96c4c8 100644 --- a/client/src/stores/useSurveyStore.tsx +++ b/client/src/stores/useSurveyStore.tsx @@ -2,52 +2,35 @@ import { create } from 'zustand'; import { combine, persist } from 'zustand/middleware'; type SurveyState = { - employeeId: string; - employeeName: string; - // REVIEW: Let's do userObjectId instead of employeeId everywhere. - // REVIEW: Similary for name. - userObjectId: string; surveyData: { objectId?: string | null; parentSurveyCode?: string | null; responses?: any; - // REVIEW: Can we be more specific with other properties? - [key: string]: any; childSurveyCodes?: string[]; + surveyCode?: string | null; } | null; }; type SurveyActions = { - setEmployeeId: (id: string) => void; - setEmployeeName: (name: string) => void; setSurveyData: (data: any | null) => void; - setParentSurveyCode: (code: string | null) => void; // Helper to set parentSurveyCode within surveyData - getParentSurveyCode: () => string | null; // Helper to get parentSurveyCode from surveyData - setObjectId: (id: string | null) => void; // Helper to set objectId within surveyData - getObjectId: () => string | null; // Helper to get objectId from surveyData - setSurveyCode: (code: string | null) => void; // Helper to set surveyCode within surveyData - getSurveyCode: () => string | null; // Helper to get surveyCode from surveyData - setChildSurveyCodes: (codes: string[]) => void; // Helper to set childSurveyCodes within surveyData - clearSession: () => void; // Clear all survey data (for logout) - clearSurvey: () => void; // Clear survey-specific data (for navigating away from survey) + setParentSurveyCode: (code: string | null) => void; + getParentSurveyCode: () => string | null; + setObjectId: (id: string | null) => void; + getObjectId: () => string | null; + setSurveyCode: (code: string | null) => void; + getSurveyCode: () => string | null; + setChildSurveyCodes: (codes: string[]) => void; + clearSession: () => void; + clearSurvey: () => void; }; export const useSurveyStore = create( persist( combine( { - employeeId: '', - employeeName: '', surveyData: null, - userObjectId: '' }, (set, get) => ({ - setUserObjectId: (userObjectId: string) => - set({ userObjectId }), - getUserObjectId: () => get().userObjectId, - setEmployeeId: (employeeId: string) => set({ employeeId }), - setEmployeeName: (employeeName: string) => - set({ employeeName }), setSurveyData: (surveyData: any | null) => set({ surveyData }), setParentSurveyCode: (parentSurveyCode: string | null) => { const currentData = get().surveyData ?? {}; @@ -70,7 +53,7 @@ export const useSurveyStore = create( set({ surveyData: { ...currentData, childSurveyCodes } }); }, clearSession: () => { - set({ employeeId: '', employeeName: '', surveyData: null }); + set({ surveyData: null }); useSurveyStore.persist.clearStorage(); }, clearSurvey: () => { diff --git a/client/src/utils/authTokenHandler.ts b/client/src/utils/authTokenHandler.ts index 2cd1692e..298055d7 100644 --- a/client/src/utils/authTokenHandler.ts +++ b/client/src/utils/authTokenHandler.ts @@ -1,10 +1,8 @@ // This file provides helper functions to manage JWTs used in authentication. -import { useAuthStore, useSurveyStore } from '@/stores'; +import { useAuthStore } from '@/stores'; import { jwtDecode } from 'jwt-decode'; interface JwtPayload { - firstName: string; - role: string; userObjectId: string; } @@ -37,42 +35,3 @@ export function hasAuthToken(): boolean { const token = getAuthToken(); return token != null && token !== ''; } - -// REVIEW: Do we need these helper functions anymore? -// REVIEW: use auth context instead of these helper functions? -export function getRole(): string { - const decodedAuthToken = getDecodedAuthToken(); - if (decodedAuthToken == null || decodedAuthToken.role == null) return ''; - return decodedAuthToken.role; -} - -export function getFirstName(): string { - const decodedAuthToken = getDecodedAuthToken(); - if (decodedAuthToken == null || decodedAuthToken.firstName == null) - return ''; - return decodedAuthToken.firstName; -} - -export function getObjectId(): string { - const decodedAuthToken = getDecodedAuthToken(); - if (decodedAuthToken == null || decodedAuthToken.userObjectId == null) - return ''; - return decodedAuthToken.userObjectId; -} - -export function initializeSurveyStore() { - const { - setEmployeeId, - setEmployeeName, - setParentSurveyCode, - setObjectId, - setChildSurveyCodes - } = useSurveyStore.getState(); - const objectId = getObjectId(); - const employeeName = getFirstName(); - setEmployeeId(objectId); - setEmployeeName(employeeName); - setParentSurveyCode(null); - setChildSurveyCodes([]); - setObjectId(null); -} diff --git a/server/src/routes/v1/auth.ts b/server/src/routes/v1/auth.ts index 4f2aa92c..3bcfcab2 100644 --- a/server/src/routes/v1/auth.ts +++ b/server/src/routes/v1/auth.ts @@ -122,11 +122,7 @@ router.post( locationObjectId }); await newUser.save(); - const token = generateAuthToken( - newUser.firstName, - newUser.role, - newUser.id - ); + const token = generateAuthToken(newUser.id); res.json({ message: 'Signup successful!', @@ -170,7 +166,7 @@ router.post( }); return; } - const token = generateAuthToken(user.firstName, user.role, user.id); + const token = generateAuthToken(user.id); res.json({ message: 'Login successful!', diff --git a/server/src/types/auth.ts b/server/src/types/auth.ts index 60698d09..60a83976 100644 --- a/server/src/types/auth.ts +++ b/server/src/types/auth.ts @@ -15,8 +15,6 @@ export interface AuthenticatedRequest extends Request { export interface JWTPayload extends JwtPayload { userObjectId: string; - role: string; - firstName: string; } export interface OTPRequest { diff --git a/server/src/utils/authTokenHandler.ts b/server/src/utils/authTokenHandler.ts index 3d7ea268..4c3c14e4 100644 --- a/server/src/utils/authTokenHandler.ts +++ b/server/src/utils/authTokenHandler.ts @@ -14,15 +14,9 @@ function getTokenSecret(): string { // Generates the JSON Web Token to be used by the client, a client having a valid JWT // means that they should be atleast a volunteer in role and must have been approved // by an admin. -export function generateAuthToken( - firstName: string, - role: string, - userObjectId: string -): string { +export function generateAuthToken(userObjectId: string): string { return jwt.sign( { - firstName: firstName, - role: role, userObjectId: userObjectId }, getTokenSecret(),