Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enregistre le mot de passe après une connexion OpenID #1044

Merged
merged 5 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading