Skip to content

Commit

Permalink
Merge pull request #1482 from jay-hodgson/SWC-7207
Browse files Browse the repository at this point in the history
  • Loading branch information
jay-hodgson authored Jan 6, 2025
2 parents 46d69b9 + dc90f9c commit 77e029e
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 11 deletions.
2 changes: 1 addition & 1 deletion apps/SageAccountWeb/src/components/RegisterAccount2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export const RegisterAccount2 = (props: RegisterAccount2Props) => {
<Typography variant="smallText1" sx={{ marginTop: '20px' }}>
Your <strong>password</strong> needs to be at least 8 letters. We
recommend using a strong, unique <strong>password</strong> of
between 16-32 characters. You can use letters, numbers, and
between 8-32 characters. You can use letters, numbers, and
punctuation marks.
</Typography>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/SageAccountWeb/src/components/ResetPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const SetPasswordInstructions = (
<Typography variant="headline2">{props.title}</Typography>
<Typography variant="smallText1">
We recommend using a strong, unique <strong>password</strong> of between
16-32 characters. A valid password must be at least 8 characters long and
8-32 characters. A valid password must be at least 8 characters long and
must include letters, digits (0-9), and special characters
~!@#$%^&*_-+=`|\(){}[]:;&quot;&apos;&lt;&gt;,.?/
</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('ChangePassword tests', () => {
)

const currentPassword = 'currentPassword'
const newPassword = 'newPassword'
const newPassword = 'newPa$$w0rd'

const {
user,
Expand All @@ -170,6 +170,29 @@ describe('ChangePassword tests', () => {

expect(usernameField).not.toBeInTheDocument() //logged in, username should not be present
await user.type(currentPasswordField, currentPassword)
await user.type(newPasswordField, 'abc')
expect(
screen.getByText('A valid password must be at least 8 characters long'),
).toBeInTheDocument()
await user.clear(newPasswordField)
await user.type(newPasswordField, '1234567$')
expect(
screen.getByText('A valid password must include letters'),
).toBeInTheDocument()
await user.clear(newPasswordField)
await user.type(newPasswordField, 'abcdefg$')
expect(
screen.getByText('A valid password must include digits (0-9)'),
).toBeInTheDocument()
await user.clear(newPasswordField)
await user.type(newPasswordField, 'abcd1234')
expect(
screen.getByText(
'A valid password must include special characters ~!@#$%^&*_-+=`|\\(){}[]:;"\'<>,.?/',
),
).toBeInTheDocument()

await user.clear(newPasswordField)
await user.type(newPasswordField, newPassword)
await user.type(confirmPasswordField, newPassword)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { MOCK_REPO_ORIGIN } from '../../utils/functions/getEndpoint'
import { MOCK_USER_ID } from '../../mocks/user/mock_user_profile'
import { getResetTwoFactorAuthHandlers } from '../../mocks/msw/handlers/resetTwoFactorAuthHandlers'

import { BrowserRouter } from 'react-router-dom'
const meta: Meta<typeof ChangePassword> = {
title: 'Authentication/ChangePassword/WithCurrentPassword',
component: ChangePassword,
Expand All @@ -20,11 +20,13 @@ const meta: Meta<typeof ChangePassword> = {
Story => {
return (
<>
This story uses mock server responses. You may need to refresh the
page to reset the mock server responses.
<Paper sx={{ my: 4, p: 4, mx: 'auto', width: '600px' }}>
<Story />
</Paper>
<BrowserRouter>
This story uses mock server responses. You may need to refresh the
page to reset the mock server responses.
<Paper sx={{ my: 4, p: 4, mx: 'auto', width: '600px' }}>
<Story />
</Paper>
</BrowserRouter>
</>
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useGetCurrentUserProfile } from '../../synapse-queries'
import { displayToast } from '../ToastMessage'
import useChangePasswordFormState from './useChangePasswordFormState'
import { useSynapseContext } from '../../utils'
import { validatePassword } from '../../utils/functions/StringUtils'

export const PASSWORD_CHANGED_SUCCESS_MESSAGE =
'Your password was successfully changed.'
Expand All @@ -18,6 +19,9 @@ export default function ChangePassword(props: ChangePasswordProps) {
const { redirectToRoute, hideReset2FA = false } = props
const [oldPassword, setOldPassword] = useState<string>('')
const [newPassword, setNewPassword] = useState<string>('')
const [newPasswordError, setNewPasswordError] = useState<string | undefined>(
undefined,
)
const [confirmPassword, setConfirmPassword] = useState<string>('')
const [userName, setUserName] = useState<string>('')
const { accessToken } = useSynapseContext()
Expand Down Expand Up @@ -105,11 +109,16 @@ export default function ChangePassword(props: ChangePasswordProps) {
<TextField
fullWidth
required
helperText={newPasswordError}
margin={'normal'}
type="password"
id="newPassword"
label={'New password'}
onChange={e => setNewPassword(e.target.value)}
onChange={e => {
const error = validatePassword(e.target.value)
setNewPasswordError(error)
setNewPassword(e.target.value)
}}
value={newPassword}
/>
<TextField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Alert, Button, TextField } from '@mui/material'
import { PasswordResetSignedToken } from '@sage-bionetworks/synapse-types'
import { displayToast } from '../ToastMessage'
import useChangePasswordFormState from './useChangePasswordFormState'
import { validatePassword } from '../../utils/functions/StringUtils'

type ChangePasswordWithTokenProps = {
passwordChangeToken: PasswordResetSignedToken
Expand All @@ -15,6 +16,9 @@ export default function ChangePasswordWithToken(
const { passwordChangeToken, onSuccess } = props
const [newPassword, setNewPassword] = useState<string>('')
const [confirmPassword, setConfirmPassword] = useState<string>('')
const [newPasswordError, setNewPasswordError] = useState<string | undefined>(
undefined,
)

const {
promptForTwoFactorAuth,
Expand Down Expand Up @@ -53,11 +57,16 @@ export default function ChangePasswordWithToken(
<TextField
fullWidth
required
helperText={newPasswordError}
type="password"
id="newPassword"
name="newPassword"
label={'New password'}
onChange={e => setNewPassword(e.target.value)}
onChange={e => {
const error = validatePassword(e.target.value)
setNewPasswordError(error)
setNewPassword(e.target.value)
}}
value={newPassword || ''}
sx={{ mb: 2 }}
/>
Expand Down
25 changes: 25 additions & 0 deletions packages/synapse-react-client/src/utils/functions/StringUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,28 @@ export function normalizeNumericId(id: string | number): number {
return parseInt(id)
}
}

// Used for client-side validation. Note, the server has the final say. Integration tested in ChangePassword.integration.test.tsx
// See https://github.com/Sage-Bionetworks/Synapse-Repository-Services/blob/develop/services/repository-managers/src/main/java/org/sagebionetworks/repo/manager/password/PasswordValidatorImpl.java#L47 for rules
export function validatePassword(newPassword: string) {
if (newPassword.trim().length < 8) {
return 'A valid password must be at least 8 characters long'
}
const hasLetter = /[a-zA-Z]/.test(newPassword) // Checks for at least one letter
if (!hasLetter) {
return 'A valid password must include letters'
}

const hasNumber = /\d/.test(newPassword) // Checks for at least one number
if (!hasNumber) {
return 'A valid password must include digits (0-9)'
}

const hasSpecialChar = /[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]/.test(
newPassword,
) // Checks for at least one special character
if (!hasSpecialChar) {
return 'A valid password must include special characters ~!@#$%^&*_-+=`|\\(){}[]:;"\'<>,.?/'
}
return undefined
}

0 comments on commit 77e029e

Please sign in to comment.