Skip to content

Commit

Permalink
Merge pull request #1044 from EcrituresNumeriques/fix/552
Browse files Browse the repository at this point in the history
  • Loading branch information
thom4parisot authored Oct 21, 2024
2 parents d0b0e4e + 5a31d37 commit ca0a048
Show file tree
Hide file tree
Showing 14 changed files with 826 additions and 1,463 deletions.
1,778 changes: 521 additions & 1,257 deletions front/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"eslint-plugin-react": "^7.27.0",
"eslint-plugin-vitest": "^0.5.4",
"jsdom": "^25.0.1",
"lodash.merge": "^4.6.2",
"prettier": "^2.3.0",
"vitest": "^2.1.2"
},
Expand Down
3 changes: 3 additions & 0 deletions front/src/components/Credentials.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ query getFullUserProfile {
_id
displayName
authType
authTypes
firstName
lastName
institution
Expand Down Expand Up @@ -79,5 +80,7 @@ mutation updateUser($user: ID!, $details: UserProfileInput!) {
mutation changePassword($old: String!, $new: String!, $user: ID!) {
changePassword(old: $old, new: $new, user: $user) {
_id
authType
authTypes
}
}
96 changes: 57 additions & 39 deletions front/src/components/Credentials.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'

Expand All @@ -7,7 +7,6 @@ import { useGraphQL } from '../helpers/graphQL'
import { changePassword as query } from './Credentials.graphql'
import styles from './credentials.module.scss'
import fieldStyles from './field.module.scss'
import UserInfos from "./UserInfos";
import Button from "./Button";
import Field from "./Field";
import clsx from 'clsx'
Expand All @@ -18,9 +17,19 @@ export default function Credentials () {
const [passwordC, setPasswordC] = useState('')
const [isUpdating, setIsUpdating] = useState(false)
const userId = useSelector(state => state.activeUser._id)
const hasExistingPassword = useSelector(state => state.activeUser.authTypes.includes('local'))
const runQuery = useGraphQL()
const { t } = useTranslation()

const canSubmit = useMemo(() => {
if (hasExistingPassword) {
return passwordO && password && passwordC && password === passwordC
}
else {
return password && passwordC && password === passwordC
}
})

const changePassword = async (e) => {
e.preventDefault()
try {
Expand All @@ -41,42 +50,51 @@ export default function Credentials () {
}

return (
<>
<UserInfos />

<section className={styles.section}>
<h2>{t('credentials.changePassword.title')}</h2>
<p>
{t('credentials.changePassword.para')}
</p>
<form className={clsx(styles.passwordForm, fieldStyles.inlineFields)} onSubmit={(e) => changePassword(e)}>
<Field
type="password"
placeholder= {t('credentials.oldPassword.placeholder')}
value={passwordO}
onChange={(e) => setPasswordO(e.target.value)}
/>
<Field
type="password"
placeholder= {t('credentials.newPassword.placeholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Field
type="password"
placeholder= {t('credentials.confirmNewPassword.placeholder')}
className={password === passwordC ? null : styles.beware}
value={passwordC}
onChange={(e) => setPasswordC(e.target.value)}
/>
<Button
disabled={!password || !passwordO || password !== passwordC}
primary={true}
>
{isUpdating ? 'Updating…' : 'Change'}
</Button>
</form>
</section>
</>
<section className={styles.section}>
<h2>{t('credentials.changePassword.title')}</h2>
<p>
{t('credentials.changePassword.para')}
</p>
<form className={clsx(styles.passwordForm, fieldStyles.inlineFields)} onSubmit={(e) => changePassword(e)} name={t('credentials.changePassword.title')}>
{hasExistingPassword && <Field
type="password"
name="old-password"
autoComplete="old-password"
placeholder= {t('credentials.oldPassword.placeholder')}
aria-label= {t('credentials.oldPassword.placeholder')}
value={passwordO}
onChange={(e) => setPasswordO(e.target.value)}
/>}
<Field
type="password"
name="new-password"
autoComplete="new-password"
placeholder= {t('credentials.newPassword.placeholder')}
aria-label= {t('credentials.newPassword.placeholder')}
minLength={6}
required={true}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Field
type="password"
name="new-password-confirmation"
autoComplete="new-password"
placeholder= {t('credentials.confirmNewPassword.placeholder')}
aria-label= {t('credentials.confirmNewPassword.placeholder')}
className={password === passwordC ? null : styles.beware}
minLength={6}
required={true}
value={passwordC}
onChange={(e) => setPasswordC(e.target.value)}
/>
<Button
disabled={!canSubmit}
primary={true}
>
{isUpdating ? 'Updating…' : 'Change'}
</Button>
</form>
</section>
)
}
88 changes: 88 additions & 0 deletions front/src/components/Credentials.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, test } from 'vitest'
import { fireEvent, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import { renderWithProviders } from '../../tests/setup.js'
import Component from './Credentials.jsx'

describe('Credentials', () => {
test('renders with OIDC', () => {
const preloadedState = { activeUser: { authType: 'oidc', authTypes: ['oidc'] } }
renderWithProviders(<Component />, { preloadedState })

expect(screen.getByRole('form')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.oldPassword.placeholder')).not.toBeInTheDocument()
expect(screen.queryByLabelText('credentials.newPassword.placeholder')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.confirmNewPassword.placeholder')).toBeInTheDocument()
})

test('renders with password only', () => {
const preloadedState = { activeUser: { authType: 'local', authTypes: ['local'] } }
renderWithProviders(<Component />, { preloadedState })

expect(screen.getByRole('form')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.oldPassword.placeholder')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.newPassword.placeholder')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.confirmNewPassword.placeholder')).toBeInTheDocument()
})

test('renders with both password and oidc', () => {
const preloadedState = { activeUser: { authType: 'oidc', authTypes: ['oidc', 'local'] } }
renderWithProviders(<Component />, { preloadedState })

expect(screen.getByRole('form')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.oldPassword.placeholder')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.newPassword.placeholder')).toBeInTheDocument()
expect(screen.queryByLabelText('credentials.confirmNewPassword.placeholder')).toBeInTheDocument()
})

test('cannot be submitted when confirmation difers from new password', async () => {
const preloadedState = { activeUser: { authType: 'local', authTypes: ['local'] } }
renderWithProviders(<Component />, { preloadedState })

screen.getByLabelText('credentials.oldPassword.placeholder').focus()
await userEvent.keyboard('aaaa')

screen.getByLabelText('credentials.newPassword.placeholder').focus()
await userEvent.keyboard('abcd')

screen.getByLabelText('credentials.confirmNewPassword.placeholder').focus()
await userEvent.keyboard('abc')

expect(screen.getByRole('button')).toBeDisabled()
})

test('can be submitted when confirmation equals new password', async () => {
const preloadedState = { activeUser: { authType: 'local', authTypes: ['local'] } }
renderWithProviders(<Component />, { preloadedState })

screen.getByLabelText('credentials.oldPassword.placeholder').focus()
await userEvent.keyboard('aaaa')

screen.getByLabelText('credentials.newPassword.placeholder').focus()
await userEvent.keyboard('abcd')

screen.getByLabelText('credentials.confirmNewPassword.placeholder').focus()
await userEvent.keyboard('abcd')

expect(screen.getByRole('button')).toBeEnabled()
})

test('can be submitted when confirmation equals new password and is oidc', async () => {
const preloadedState = { activeUser: { authType: 'local', authTypes: ['oidc'] } }
renderWithProviders(<Component />, { preloadedState })

screen.getByLabelText('credentials.newPassword.placeholder').focus()
await userEvent.keyboard('abcd')

screen.getByLabelText('credentials.confirmNewPassword.placeholder').focus()
await userEvent.keyboard('abcd')

expect(screen.getByRole('button')).toBeEnabled()
fireEvent.click(screen.getByRole('button'))

expect(fetch).toHaveBeenLastCalledWith(undefined, expect.objectContaining({
body: expect.stringMatching(/query":"mutation changePassword/)
}))
})
})
76 changes: 41 additions & 35 deletions front/src/createReduxStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function toWebsocketEndpoint (endpoint) {
}

// Définition du store Redux et de l'ensemble des actions
const initialState = {
export const initialState = {
hasBooted: false,
sessionToken: localStorage.getItem(sessionTokenName),
workingArticle: {
Expand Down Expand Up @@ -64,6 +64,8 @@ const initialState = {
},
// Active user (authenticated)
activeUser: {
authType: null,
authTypes: [],
zoteroToken: null,
selectedTagIds: [],
workspaces: [],
Expand All @@ -83,45 +85,47 @@ const initialState = {
}
}

const reducer = createReducer(initialState, {
APPLICATION_CONFIG: setApplicationConfig,
PROFILE: setProfile,
CLEAR_ZOTERO_TOKEN: clearZoteroToken,
LOGIN: loginUser,
UPDATE_SESSION_TOKEN: setSessionToken,
UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
LOGOUT: logoutUser,
function createRootReducer (state) {
return createReducer(state, {
APPLICATION_CONFIG: setApplicationConfig,
PROFILE: setProfile,
CLEAR_ZOTERO_TOKEN: clearZoteroToken,
LOGIN: loginUser,
UPDATE_SESSION_TOKEN: setSessionToken,
UPDATE_ACTIVE_USER_DETAILS: updateActiveUserDetails,
LOGOUT: logoutUser,

// article reducers
UPDATE_ARTICLE_STATS: updateArticleStats,
UPDATE_ARTICLE_STRUCTURE: updateArticleStructure,
UPDATE_ARTICLE_WRITERS: updateArticleWriters,
// article reducers
UPDATE_ARTICLE_STATS: updateArticleStats,
UPDATE_ARTICLE_STRUCTURE: updateArticleStructure,
UPDATE_ARTICLE_WRITERS: updateArticleWriters,

// user preferences reducers
USER_PREFERENCES_TOGGLE: toggleUserPreferences,
// user preferences reducers
USER_PREFERENCES_TOGGLE: toggleUserPreferences,

SET_ARTICLE_VERSIONS: setArticleVersions,
SET_WORKING_ARTICLE_UPDATED_AT: setWorkingArticleUpdatedAt,
SET_WORKING_ARTICLE_TEXT: setWorkingArticleText,
SET_WORKING_ARTICLE_METADATA: setWorkingArticleMetadata,
SET_WORKING_ARTICLE_BIBLIOGRAPHY: setWorkingArticleBibliography,
SET_WORKING_ARTICLE_STATE: setWorkingArticleState,
SET_CREATE_ARTICLE_VERSION_ERROR: setCreateArticleVersionError,
SET_ARTICLE_VERSIONS: setArticleVersions,
SET_WORKING_ARTICLE_UPDATED_AT: setWorkingArticleUpdatedAt,
SET_WORKING_ARTICLE_TEXT: setWorkingArticleText,
SET_WORKING_ARTICLE_METADATA: setWorkingArticleMetadata,
SET_WORKING_ARTICLE_BIBLIOGRAPHY: setWorkingArticleBibliography,
SET_WORKING_ARTICLE_STATE: setWorkingArticleState,
SET_CREATE_ARTICLE_VERSION_ERROR: setCreateArticleVersionError,

ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,
ARTICLE_PREFERENCES_TOGGLE: toggleArticlePreferences,

UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,
UPDATE_EDITOR_CURSOR_POSITION: updateEditorCursorPosition,

SET_WORKSPACES: setWorkspaces,
SET_ACTIVE_WORKSPACE: setActiveWorkspace,
SET_WORKSPACES: setWorkspaces,
SET_ACTIVE_WORKSPACE: setActiveWorkspace,

UPDATE_SELECTED_TAG: updateSelectedTag,
TAG_CREATED: tagCreated,
UPDATE_SELECTED_TAG: updateSelectedTag,
TAG_CREATED: tagCreated,

SET_LATEST_CORPUS_DELETED: setLatestCorpusDeleted,
SET_LATEST_CORPUS_CREATED: setLatestCorpusCreated,
SET_LATEST_CORPUS_UPDATED: setLatestCorpusUpdated,
})
SET_LATEST_CORPUS_DELETED: setLatestCorpusDeleted,
SET_LATEST_CORPUS_CREATED: setLatestCorpusCreated,
SET_LATEST_CORPUS_UPDATED: setLatestCorpusUpdated,
})
}

const createNewArticleVersion = store => {
return next => {
Expand Down Expand Up @@ -511,6 +515,8 @@ function setLatestCorpusUpdated (state, { data }) {

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

export default () => createStore(reducer, composeEnhancers(
applyMiddleware(createNewArticleVersion, persistStateIntoLocalStorage)
))
export default function createReduxStore (state = initialState) {
return createStore(createRootReducer(state), composeEnhancers(
applyMiddleware(createNewArticleVersion, persistStateIntoLocalStorage)
))
}
Loading

0 comments on commit ca0a048

Please sign in to comment.