Skip to content

Commit

Permalink
Added reset password, which required a lot of changes and refactors
Browse files Browse the repository at this point in the history
This was a very messy merge...
This is 95% done, but checking in so that we don't diverge so much again.
We diverged a bit with admin rights for sessions.  I left all the new stuff in parallel with the old stuff (server and client) and I'll migrate over to the new way represented as captureAccessRightsData on the client side.
Still need to change over the MUI styles in the ResetYourPassword.jsx from MUI templates to our styles (they look different, and the difference does not add value).
Navigating to /Tasks currently leaves the HeaderBar menu confused, will fix this.
Spurious "Please sign in" after resetting password, will fix this.
More testing needed -- the steps are pretty complex, with the need for immediate `useRef()` variables in some places to avoid (always losing) race conditions.
  • Loading branch information
SailingSteve committed Feb 25, 2025
1 parent 3d9688d commit e598d5a
Show file tree
Hide file tree
Showing 18 changed files with 1,888 additions and 1,613 deletions.
12 changes: 12 additions & 0 deletions src/js/common/utils/logging.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ export function authLog (text, res) {
}
}

// Log reactQuery fetches and responses
export function reactQueryLog (text, res) {
if (webAppConfig.LOG_REACT_QUERY_EVENTS) {
if (res) {
console.log('>> reactQuery >> ', text, res);
} else {
console.log('>> reactQuery >> ', text);
}
}
}


// Cordova offsets
export function cordovaOffsetLog (text, res) {
if (webAppConfig.LOG_CORDOVA_OFFSETS) {
Expand Down
179 changes: 179 additions & 0 deletions src/js/components/Login/ResetYourPassword.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { Modal } from '@mui/material';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import TextField from '@mui/material/TextField';
import PropTypes from 'prop-types';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import validator from 'validator';
import { renderLog } from '../../common/utils/logging';
import { useConnectAppContext } from '../../contexts/ConnectAppContext';
import makeRequestParams from '../../react-query/makeRequestParams';
import { useLogoutMutation, usePersonRetrieveByEmailMutation, usePersonSaveForAuthMutation } from '../../react-query/mutations';
import weConnectQueryFn, { METHOD } from '../../react-query/WeConnectQuery';
import { ErrorMessage } from '../Style/sharedStyles';
import VerifySecretCodeModal from '../VerifySecretCodeModal';

const ResetYourPassword = ({ openDialog, closeDialog }) => {
renderLog('ResetYourPassword');
const { mutate: mutateRetrievePersonByEmail } = usePersonRetrieveByEmailMutation();
const { mutate: mutatePersonSaveForAuth } = usePersonSaveForAuthMutation();
const { mutate: mutateLogout } = useLogoutMutation();
const { getAppContextValue, setAppContextValue } = useConnectAppContext();

const [open, setOpen] = React.useState(openDialog);
const [displayEmailAddress, setDisplayEmailAddress] = useState(true);
const [warningLine, setWarningLine] = useState('');
const [errorMessage, setErrorMessage] = useState('');

const emailRef = useRef('');
const password1Ref = useRef('');
const password2Ref = useRef('');
const authPerson = useRef(undefined);

useEffect(() => {
setOpen(openDialog);
}, [openDialog]);

const secretCodeVerified = getAppContextValue('secretCodeVerifiedForReset') || false;
useEffect(() => {
if (secretCodeVerified === true) {
console.log('received new secretCodeVerifiedForReset', secretCodeVerified);
setDisplayEmailAddress(false);
emailRef.current = '';
password1Ref.current = '';
password2Ref.current = '';
}
}, [secretCodeVerified]);

const auth = getAppContextValue('authenticatedPerson');
useEffect(() => {
const authP = getAppContextValue('authenticatedPerson');
if (authP && open) {
console.log('received new authP', authP);
authPerson.current = authP;
console.log('authPerson.personId in Login useEffect [auth] id: ', authP.personId);
console.log('authPerson.personId in Login useEffect [auth] open: ', open);
weConnectQueryFn('send-email-code', { personId: authP.personId }, METHOD.POST)
.then(setAppContextValue('openVerifySecretCodeModalDialog', true));
}
// eslint would have us add getAppContextValue and setAppContextValue, which causes and endless loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [auth]);

const sendEmail = async () => {
const email = emailRef.current.value;
setAppContextValue('resetEmail', email);
console.log('email in sendEmail: ', emailRef.current.value);
if (!validator.isEmail(email)) {
setWarningLine('Please enter a valid email address.');
return;
}
setWarningLine('');
setAppContextValue('openVerifySecretCodeModalDialog', true);
// Logout so that the current sessionID will not be reused when resetting password for a potentially differnt staff member
await mutateLogout();
// This retrieve will set the 'authenticatedPerson' app context value, and bring back a new sessionID (without touching the cookie)
await mutateRetrievePersonByEmail({ emailPersonal: email });
};

const handleClose = () => {
setOpen(false);
closeDialog(false);
};

const changePassword = async () => {
const pass1 = password1Ref.current.value;
const pass2 = password2Ref.current.value;
const person = authPerson.current;

console.log('password in changePassword: ', pass1, pass2);
if (pass1 !== pass2) {
setErrorMessage('Your password entries do not match.');
} else {
setErrorMessage('');
// const person = getAppContextValue('authenticatedPerson');
await mutatePersonSaveForAuth(makeRequestParams({ id: person.id }, { password: pass1 }));
setAppContextValue('isAuthenticated', true);
console.log('ResetYourPassword changePassword pass1, pass2: ', pass1, pass2);
setAppContextValue('resetPassword', pass1);
handleClose();
}
};

console.log('ResetYourPassword incoming authPerson: ', authPerson);
return (
<>
<Modal
open={open}
// onClose={handleClose}
aria-labelledby="parent-modal-title"
aria-describedby="parent-modal-description"
>
<Dialog open={open} onClose={handleClose}>
<DialogTitle sx={{ width: '400px' }}>Reset your Password</DialogTitle>
<div id="warningLine" style={{ color: 'red', padding: '10px 0 0 30px', paddingBottom: '20px' }}>{warningLine}</div>
<DialogContent>
<DialogContentText sx={{ marginBottom: '15px' }}>
{displayEmailAddress ? 'Please enter your email address.' : 'Please enter your new password.'}
</DialogContentText>
<ErrorMessage>{errorMessage}</ErrorMessage>
{ displayEmailAddress ? (
<TextField
autoFocus
fullWidth
id="email"
inputRef={emailRef}
label="Email Address"
margin="dense"
name="email"
required
type="email"
variant="standard"
/>
) : (
<form>
<TextField
autoFocus
fullWidth
id="field1"
inputRef={password1Ref}
label="Password"
margin="dense"
name="password1"
required
type="password"
variant="standard"
/>
<TextField
fullWidth
id="field2"
inputRef={password2Ref}
label="Verify Password"
margin="dense"
name="password2"
required
type="password"
variant="standard"
/>
</form>
)}
<Button sx={{ float: 'right' }} onClick={displayEmailAddress ? sendEmail : changePassword}>
{displayEmailAddress ? 'Send reset email' : 'Save your new password'}
</Button>
</DialogContent>
</Dialog>
</Modal>
<VerifySecretCodeModal person={authPerson.current} />
</>
);
};
ResetYourPassword.propTypes = {
openDialog: PropTypes.func,
closeDialog: PropTypes.func,
};

export default ResetYourPassword;
8 changes: 4 additions & 4 deletions src/js/components/Navigation/HeaderBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import styled from 'styled-components';
import standardBoxShadow from '../../common/components/Style/standardBoxShadow';
import { hasIPhoneNotch } from '../../common/utils/cordovaUtils';
import { normalizedHrefPage } from '../../common/utils/hrefUtils';
import { renderLog } from '../../common/utils/logging';
import { authLog, renderLog } from '../../common/utils/logging';
import { useConnectAppContext } from '../../contexts/ConnectAppContext';
import { clearSignedInGlobals } from '../../contexts/contextFunctions';
import { viewerCanSeeOrDo } from '../../models/AuthModel';
Expand All @@ -22,7 +22,7 @@ import HeaderBarLogo from './HeaderBarLogo';
const HeaderBar = ({ hideTabs }) => {
renderLog('HeaderBar');
const navigate = useNavigate();
const { apiDataCache, getAppContextValue, setAppContextValue } = useConnectAppContext();
const { apiDataCache, getAppContextValue, setAppContextValue, getAppContextData } = useConnectAppContext();
const { viewerAccessRights } = apiDataCache;
const { mutate: mutateLogout } = useLogoutMutation();

Expand All @@ -34,7 +34,7 @@ const HeaderBar = ({ hideTabs }) => {
const isAuth = getAppContextValue('isAuthenticated');
useEffect(() => {
if (isAuth !== null) {
console.log('----------- isAuthenticated changed =', isAuth);
authLog('HeaderBar isAuthenticated changed =', isAuth);
setIsAuthenticated(isAuth);
}
}, [isAuth]);
Expand All @@ -43,7 +43,7 @@ const HeaderBar = ({ hideTabs }) => {
// I don't think we want to make the weConnectQueryFn call here since we are about to call mutateLogout
const data = await weConnectQueryFn('logout', {}, METHOD.POST);
console.log(`/logout response in HeaderBar -- status: '${'status'}', data: ${JSON.stringify(data)}`);
clearSignedInGlobals(setAppContextValue);
clearSignedInGlobals(setAppContextValue, getAppContextData);
navigate('/login');
mutateLogout();
};
Expand Down
6 changes: 1 addition & 5 deletions src/js/components/Person/PersonSummaryRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import styled from 'styled-components';
import DesignTokenColors from '../../common/components/Style/DesignTokenColors';
import { renderLog } from '../../common/utils/logging';
import { useConnectAppContext } from '../../contexts/ConnectAppContext';
import {
getFullNamePreferredPerson,
// useGetFullNamePreferred,
} from '../../models/PersonModel';
import { getFullNamePreferredPerson } from '../../models/PersonModel';
import { useRemoveTeamMemberMutation } from '../../react-query/mutations';
import { DeleteStyled, EditStyled } from '../Style/iconStyles';
import { viewerCanSeeOrDo, viewerCanSeeOrDoForThisTeam } from '../../models/AuthModel';
Expand Down Expand Up @@ -117,7 +114,6 @@ const PersonSummaryRow = ({ person, rowNumberForDisplay, teamId }) => {
id={`removeMember-personId-${person.personId}`}
onClick={() => removeTeamMemberClick(person)}
style={{ cursor: 'pointer' }}
// cellwidth="20"
cellwidth={20}
>
<DeleteStyled />
Expand Down
9 changes: 4 additions & 5 deletions src/js/components/PrivateRoute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,24 @@ import { captureAccessRightsData } from '../models/AuthModel';

const PrivateRoute = () => {
const location = useLocation();
const { apiDataCache, getAppContextValue } = useConnectAppContext();
const { apiDataCache, getAppContextValue, setAppContextValue } = useConnectAppContext();
const dispatch = useConnectDispatch();

const [isAuthenticated, setIsAuthenticated] = useState(null);

const { data: dataAuth, isSuccess: isSuccessAuth } = useFetchData(['get-auth'], {}, METHOD.POST);
useEffect(() => {
if (isSuccessAuth) {
// console.log('useFetchData in PrivateRoute useEffect dataAuth good:', dataAuth, isSuccessAuth);
authLog('useFetchData in PrivateRoute useEffect dataAuth good:', dataAuth, isSuccessAuth);
setIsAuthenticated(dataAuth.isAuthenticated);
// setAppContextValue('loggedInPersonIsAdmin', dataAuth.loggedInPersonIsAdmin);
setAppContextValue('loggedInPersonIsAdmin', dataAuth.loggedInPersonIsAdmin);
captureAccessRightsData(dataAuth, isSuccessAuth, apiDataCache, dispatch);
authLog('========= PrivateRoute =========== INNER isAuthenticated: ', dataAuth.isAuthenticated);
}
}, [dataAuth, isSuccessAuth]);

const isAuth = getAppContextValue('isAuthenticated');

authLog('========= PrivateRoute =========== OUTER isAuthenticated: ', isAuthenticated, ', isAuth: ', isAuth);
authLog('========= PrivateRoute =========== isAuthenticated: ', isAuthenticated);

if (isAuthenticated || isAuth || isAuthenticated !== false) {
return <Outlet />;
Expand Down
7 changes: 7 additions & 0 deletions src/js/components/Style/sharedStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ export const MatchingPerson = styled('div')`
margin: 10px 0 0 10px;
font-style: italic;
`;

export const ErrorMessage = styled('div')`
color: red;
margin-top: 50px;
text-align: center;
font-size: 18px;
`;
20 changes: 9 additions & 11 deletions src/js/components/Team/TeamHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const TeamHeader = ({ showHeaderLabels, showIcons, team }) => {
return (
<OneTeamHeader>
{/* Width (below) of this TeamHeaderCell comes from the combined widths of the first x columns in TeamMemberList */}
<TeamHeaderCell cellwidth={215} largefont="true" titlecell="true">
<TeamHeaderCell cellwidth={215} $largefont $titlecell>
{teamLocal && (
<Link to={`/team-home/${teamLocal.id}`}>
{teamLocal.teamName}
Expand Down Expand Up @@ -85,19 +85,17 @@ const OneTeamHeader = styled('div')`
margin-top: 10px;
`;

const TeamHeaderCell = styled('div', {
shouldForwardProp: (prop) => !['largefont', 'titlecell', 'cellwidth'].includes(prop),
})(({ largefont, titlecell, cellwidth }) => (`
const TeamHeaderCell = styled.div`
align-content: center;
${(titlecell) ? '' : 'border-bottom: 1px solid #ccc;'}
${(largefont) ? 'font-size: 1.1em;' : 'font-size: .8em;'}
${(titlecell) ? '' : 'font-weight: 550;'}
border-bottom: ${(props) => (props?.$titleCell ? ';' : '1px solid #ccc;')}
font-size: ${(props) => (props?.$largefont ? '1.1em;' : '.8em;')};
font-weight: ${(props) => (props?.$titleCell ? ';' : '550;')}
height: 22px;
${cellwidth ? `max-width: ${cellwidth}px;` : ''}
${cellwidth ? `min-width: ${cellwidth}px;` : ''}
max-width: ${(props) => (props.cellwidth ? `${props.cellwidth}px;` : ';')};
min-width: ${(props) => (props.cellwidth ? `${props.cellwidth}px;` : ';')};
width: ${(props) => (props.cellwidth ? `${props.cellwidth}px;` : ';')};
overflow: hidden;
white-space: nowrap;
${cellwidth ? `width: ${cellwidth}px;` : ''}
`));
`;

export default withStyles(styles)(TeamHeader);
Loading

0 comments on commit e598d5a

Please sign in to comment.