Skip to content

Commit

Permalink
Reset password works when signed in or not signed in
Browse files Browse the repository at this point in the history
The race conditions and hanging tab (which was due to a strange bug if you use a <form> with out a submit in React).
There are still some clean up items to do:
1) Create person when an admin, without requiring a password verification -- but in any case a password verfication will still be required on the first login by the new staff person.
2) Escaping out of the creation/verification/reset process without completing all the steps
3) Staying signed in after a password reset

This is still an ongoing development ... only bother merging for now, if you have merges you want to commit.
  • Loading branch information
SailingSteve committed Mar 2, 2025
1 parent 14fb862 commit 926a692
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 114 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"react/destructuring-assignment": 0, // Dec 2018: We should do this! But right now we have 3990 warnings/errors if enabled.
"react/forbid-prop-types": 0, // Dec 2018: Should consider someday
"react/indent-prop": 0,
"react/jsx-curly-newline": 0, // Feb 2025, started showing up with latest eslint
"react/jsx-first-prop-new-line": 0,
"react/jsx-indent-props": 0,
"react/jsx-no-bind": 1, // Dec 2018: Should these be errors?
Expand Down
2 changes: 1 addition & 1 deletion src/js/common/utils/logging.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function httpLog (text, res) {
}
}

// Log oAuth steps
// Log auth steps
export function authLog (text, res) {
if (webAppConfig.LOG_AUTHENTICATION) {
if (res) {
Expand Down
92 changes: 55 additions & 37 deletions src/js/components/Login/ResetYourPassword.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,33 @@ import TextField from '@mui/material/TextField';
import PropTypes from 'prop-types';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
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 { useLogoutMutation, usePasswordSaveMutation, usePersonRetrieveByEmailMutation } 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: mutatePasswordSave } = usePasswordSaveMutation();
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 [personId, setPersonId] = useState('');

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

useEffect(() => {
setOpen(openDialog);
Expand All @@ -43,6 +45,7 @@ const ResetYourPassword = ({ openDialog, closeDialog }) => {
if (secretCodeVerified === true) {
console.log('received new secretCodeVerifiedForReset', secretCodeVerified);
setDisplayEmailAddress(false);
emailDisabledRef.current = authPersonRef.current?.emailPersonal || '';
emailRef.current = '';
password1Ref.current = '';
password2Ref.current = '';
Expand All @@ -52,33 +55,19 @@ const ResetYourPassword = ({ openDialog, closeDialog }) => {
const auth = getAppContextValue('authenticatedPerson');
useEffect(() => {
const authP = getAppContextValue('authenticatedPerson');
authPersonRef.current = authP;
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);
console.log('authPersonRef.personId in Login useEffect [auth] id: ', authP.personId);
console.log('authPersonRef.personId in Login useEffect [auth] open: ', open);
setPersonId(authP.personId);
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);
Expand All @@ -88,23 +77,41 @@ const ResetYourPassword = ({ openDialog, closeDialog }) => {
const changePassword = async () => {
const pass1 = password1Ref.current.value;
const pass2 = password2Ref.current.value;
const person = authPerson.current;
const person = authPersonRef.current;
let id;
if (person?.id) {
id = person.id;
} else {
id = personId;
}

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 }));
await mutatePasswordSave({ personId: id, password: pass1 });
setAppContextValue('isAuthenticated', true);
console.log('ResetYourPassword changePassword pass1, pass2: ', pass1, pass2);
setAppContextValue('authenticatedPerson', person);
setAppContextValue('resetPassword', pass1);
handleClose();
}
};

console.log('ResetYourPassword incoming authPerson: ', authPerson);
const sendEmail = async () => {
const email = emailRef.current.value;
setAppContextValue('resetEmail', email);
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 });
};

return (
<>
<Modal
Expand Down Expand Up @@ -132,10 +139,12 @@ const ResetYourPassword = ({ openDialog, closeDialog }) => {
name="email"
required
type="email"
variant="standard"
variant="outlined"
/>
) : (
<form>
<>
{/* EmailDiv clues in Google "Update password?" dialog to display this email, it probably could be css hidden */}
<EmailDiv id="username">Email: &nbsp;&nbsp;&nbsp;{emailDisabledRef.current}</EmailDiv>
<TextField
autoFocus
fullWidth
Expand All @@ -145,8 +154,7 @@ const ResetYourPassword = ({ openDialog, closeDialog }) => {
margin="dense"
name="password1"
required
type="password"
variant="standard"
variant="outlined"
/>
<TextField
fullWidth
Expand All @@ -156,24 +164,34 @@ const ResetYourPassword = ({ openDialog, closeDialog }) => {
margin="dense"
name="password2"
required
type="password"
variant="standard"
variant="outlined"
/>
</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} />
<VerifySecretCodeModal person={authPersonRef.current} />
</>
);
};
ResetYourPassword.propTypes = {
openDialog: PropTypes.func,
openDialog: PropTypes.bool,
closeDialog: PropTypes.func,
};

const EmailDiv = styled('div')`
padding: 0 0 20px 0;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-weight: 400;
font-size: 1rem;
line-height: 1.5;
letter-spacing: 0.00938em;
color: rgba(0, 0, 0, 0.6);
`;


export default ResetYourPassword;
26 changes: 22 additions & 4 deletions src/js/components/Navigation/HeaderBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { Button, Tab, Tabs } from '@mui/material';
import { withStyles } from '@mui/styles';
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { useLocation, useNavigate } from 'react-router';
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 { authLog, renderLog } from '../../common/utils/logging';
import { authLog, renderLog, routingLog } from '../../common/utils/logging';
import { useConnectAppContext } from '../../contexts/ConnectAppContext';
import { clearSignedInGlobals } from '../../contexts/contextFunctions';
import { viewerCanSeeOrDo } from '../../models/AuthModel';
Expand All @@ -23,13 +23,13 @@ const HeaderBar = ({ hideTabs }) => {
renderLog('HeaderBar');
const navigate = useNavigate();
const { apiDataCache, getAppContextValue, setAppContextValue, getAppContextData } = useConnectAppContext();
const { viewerAccessRights } = apiDataCache;
const { mutate: mutateLogout } = useLogoutMutation();

const [scrolledDown] = useState(false);
const [tabsValue, setTabsValue] = useState('1');
const [showTabs, setShowTabs] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [viewerAccessRights, setViewerAccessRights] = useState(apiDataCache);

const isAuth = getAppContextValue('isAuthenticated');
useEffect(() => {
Expand Down Expand Up @@ -73,8 +73,16 @@ const HeaderBar = ({ hideTabs }) => {
}
};

const authP = getAppContextValue('authenticatedPerson');
useEffect(() => {
// Track new user logging in, possibly after a reset password, and display the resulting appropriate tabs
console.log('useEffect authenticatedPerson changed');
setViewerAccessRights(apiDataCache);
initializeTabValue();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authP]);

const handleTabChange = (event, newValue) => {
// console.log(`handleTabChange newValue: ${newValue}`);
// setTabsValue(newValue);
if (newValue) {
switch (newValue) {
Expand Down Expand Up @@ -124,6 +132,16 @@ const HeaderBar = ({ hideTabs }) => {
initializeTabValue();
}, []);

// useNavigate() called from anywhere, will update the ReactRouter, and will call initializeTabValue()
const loc = useLocation();
React.useEffect(() => {
routingLog('HeaderBar useLocation detected a url change to: ', loc.pathname);
setViewerAccessRights(apiDataCache);
initializeTabValue();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loc]);


return (
<HeaderBarWrapper
$hasNotch={hasIPhoneNotch()}
Expand Down
5 changes: 4 additions & 1 deletion src/js/components/VerifySecretCodeModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const VerifySecretCodeModal = ({ classes, person }) => {
useEffect(() => {
setOpenDialogMutable(getAppContextValue('openVerifySecretCodeModalDialog'));
setAppContextValue('secretCodeVerified', false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);

const handleClose = () => {
Expand All @@ -54,7 +55,8 @@ const VerifySecretCodeModal = ({ classes, person }) => {
code += refDigit.current.value.toString();
}

const data = await weConnectQueryFn('verify-email-code', { personId: person.personId, code }, METHOD.POST);
const id = person.personId || person.id;
const data = await weConnectQueryFn('verify-email-code', { personId: id, code }, METHOD.POST);
console.log(`/verify-email-code response: data: ${JSON.stringify(data)}`);
if (data.emailVerified) {
setAppContextValue('secretCodeVerified', true);
Expand Down Expand Up @@ -85,6 +87,7 @@ const VerifySecretCodeModal = ({ classes, person }) => {
}, 50);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nextFocus]);

const extractDigits = (str) => {
Expand Down
7 changes: 5 additions & 2 deletions src/js/contexts/ConnectAppContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,15 @@ export const ConnectAppContextProvider = ({ children }) => {
authLog('useFetchData in ConnectAppContext useEffect dataAuth good:', dataAuth, isSuccessAuth, isFetchingAuth);
const { isAuthenticated } = dataAuth;
setAppContextValue('authenticatedPerson', dataAuth.person);
setAppContextValue('isAuthenticated', isAuthenticated);
setAppContextValue('loggedInPersonIsAdmin', dataAuth.loggedInPersonIsAdmin);
if (dataAuth.person) {
setAppContextValue('isAuthenticated', isAuthenticated);
setAppContextValue('loggedInPersonIsAdmin', dataAuth.loggedInPersonIsAdmin);
}
captureAccessRightsData(dataAuth, isSuccessAuth, apiDataCache, dispatch);

console.log('=============== ConnectAppContextProvider ======= isAuthenticated: ', isAuthenticated, ' ===========');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataAuth, isSuccessAuth]);

return (
Expand Down
Loading

0 comments on commit 926a692

Please sign in to comment.