mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	[PUI] Set password (#8770)
* Add <ChangePassword> page * Rename Set-Password to ResetPassword * Add unit testing * Ensure user is properly logged into page * Update playwright tests * Small tweaks
This commit is contained in:
		| @@ -75,7 +75,7 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug | |||||||
|                 'key': 'dynamic-panel', |                 'key': 'dynamic-panel', | ||||||
|                 'title': 'Dynamic Panel', |                 'title': 'Dynamic Panel', | ||||||
|                 'source': self.plugin_static_file('sample_panel.js'), |                 'source': self.plugin_static_file('sample_panel.js'), | ||||||
|                 'icon': 'part', |                 'icon': 'ti:wave-saw-tool:outline', | ||||||
|                 'context': { |                 'context': { | ||||||
|                     'version': INVENTREE_SW_VERSION, |                     'version': INVENTREE_SW_VERSION, | ||||||
|                     'plugin_version': self.VERSION, |                     'plugin_version': self.VERSION, | ||||||
| @@ -97,7 +97,7 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug | |||||||
|                 'key': 'part-panel', |                 'key': 'part-panel', | ||||||
|                 'title': _('Part Panel'), |                 'title': _('Part Panel'), | ||||||
|                 'source': self.plugin_static_file('sample_panel.js:renderPartPanel'), |                 'source': self.plugin_static_file('sample_panel.js:renderPartPanel'), | ||||||
|                 'icon': 'part', |                 'icon': 'ti:package_outline', | ||||||
|                 'context': {'part_name': part.name if part else ''}, |                 'context': {'part_name': part.name if part else ''}, | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ export enum ApiEndpoints { | |||||||
|   user_simple_login = 'email/generate/', |   user_simple_login = 'email/generate/', | ||||||
|   user_reset = 'auth/password/reset/', |   user_reset = 'auth/password/reset/', | ||||||
|   user_reset_set = 'auth/password/reset/confirm/', |   user_reset_set = 'auth/password/reset/confirm/', | ||||||
|  |   user_change_password = 'auth/password/change/', | ||||||
|   user_sso = 'auth/social/', |   user_sso = 'auth/social/', | ||||||
|   user_sso_remove = 'auth/social/:id/disconnect/', |   user_sso_remove = 'auth/social/:id/disconnect/', | ||||||
|   user_emails = 'auth/emails/', |   user_emails = 'auth/emails/', | ||||||
|   | |||||||
							
								
								
									
										124
									
								
								src/frontend/src/pages/Auth/ChangePassword.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/frontend/src/pages/Auth/ChangePassword.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | |||||||
|  | import { Trans, t } from '@lingui/macro'; | ||||||
|  | import { | ||||||
|  |   Button, | ||||||
|  |   Center, | ||||||
|  |   Container, | ||||||
|  |   Divider, | ||||||
|  |   Group, | ||||||
|  |   Paper, | ||||||
|  |   PasswordInput, | ||||||
|  |   Stack, | ||||||
|  |   Text | ||||||
|  | } from '@mantine/core'; | ||||||
|  | import { useForm } from '@mantine/form'; | ||||||
|  | import { notifications } from '@mantine/notifications'; | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
|  |  | ||||||
|  | import { api } from '../../App'; | ||||||
|  | import { StylishText } from '../../components/items/StylishText'; | ||||||
|  | import { ProtectedRoute } from '../../components/nav/Layout'; | ||||||
|  | import { LanguageContext } from '../../contexts/LanguageContext'; | ||||||
|  | import { ApiEndpoints } from '../../enums/ApiEndpoints'; | ||||||
|  | import { apiUrl } from '../../states/ApiState'; | ||||||
|  | import { useUserState } from '../../states/UserState'; | ||||||
|  |  | ||||||
|  | export default function Set_Password() { | ||||||
|  |   const simpleForm = useForm({ | ||||||
|  |     initialValues: { | ||||||
|  |       new_password1: '', | ||||||
|  |       new_password2: '' | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const user = useUserState(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |   function passwordError(values: any) { | ||||||
|  |     let message: any = | ||||||
|  |       values?.new_password2 || | ||||||
|  |       values?.new_password1 || | ||||||
|  |       values?.error || | ||||||
|  |       t`Password could not be changed`; | ||||||
|  |  | ||||||
|  |     // If message is array | ||||||
|  |     if (!Array.isArray(message)) { | ||||||
|  |       message = [message]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     message.forEach((msg: string) => { | ||||||
|  |       notifications.show({ | ||||||
|  |         title: t`Error`, | ||||||
|  |         message: msg, | ||||||
|  |         color: 'red' | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleSet() { | ||||||
|  |     // Set password with call to backend | ||||||
|  |     api | ||||||
|  |       .post(apiUrl(ApiEndpoints.user_change_password), { | ||||||
|  |         new_password1: simpleForm.values.new_password1, | ||||||
|  |         new_password2: simpleForm.values.new_password2 | ||||||
|  |       }) | ||||||
|  |       .then((val) => { | ||||||
|  |         if (val.status === 200) { | ||||||
|  |           notifications.show({ | ||||||
|  |             title: t`Password Changed`, | ||||||
|  |             message: t`The password was set successfully. You can now login with your new password`, | ||||||
|  |             color: 'green', | ||||||
|  |             autoClose: false | ||||||
|  |           }); | ||||||
|  |           navigate('/login'); | ||||||
|  |         } else { | ||||||
|  |           passwordError(val.data); | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .catch((err) => { | ||||||
|  |         passwordError(err.response.data); | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <LanguageContext> | ||||||
|  |       <ProtectedRoute> | ||||||
|  |         <Center mih='100vh'> | ||||||
|  |           <Container w='md' miw={425}> | ||||||
|  |             <Stack> | ||||||
|  |               <StylishText size='xl'>{t`Reset Password`}</StylishText> | ||||||
|  |               <Divider /> | ||||||
|  |               {user.username() && ( | ||||||
|  |                 <Paper> | ||||||
|  |                   <Group> | ||||||
|  |                     <StylishText size='md'>{t`User`}</StylishText> | ||||||
|  |                     <Text>{user.username()}</Text> | ||||||
|  |                   </Group> | ||||||
|  |                 </Paper> | ||||||
|  |               )} | ||||||
|  |               <Divider /> | ||||||
|  |               <Stack gap='xs'> | ||||||
|  |                 <PasswordInput | ||||||
|  |                   required | ||||||
|  |                   aria-label='input-password-1' | ||||||
|  |                   label={t`New Password`} | ||||||
|  |                   description={t`Enter your new password`} | ||||||
|  |                   {...simpleForm.getInputProps('new_password1')} | ||||||
|  |                 /> | ||||||
|  |                 <PasswordInput | ||||||
|  |                   required | ||||||
|  |                   aria-label='input-password-2' | ||||||
|  |                   label={t`Confirm New Password`} | ||||||
|  |                   description={t`Confirm your new password`} | ||||||
|  |                   {...simpleForm.getInputProps('new_password2')} | ||||||
|  |                 /> | ||||||
|  |               </Stack> | ||||||
|  |               <Button type='submit' onClick={handleSet}> | ||||||
|  |                 <Trans>Confirm</Trans> | ||||||
|  |               </Button> | ||||||
|  |             </Stack> | ||||||
|  |           </Container> | ||||||
|  |         </Center> | ||||||
|  |       </ProtectedRoute> | ||||||
|  |     </LanguageContext> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -17,7 +17,7 @@ import { LanguageContext } from '../../contexts/LanguageContext'; | |||||||
| import { ApiEndpoints } from '../../enums/ApiEndpoints'; | import { ApiEndpoints } from '../../enums/ApiEndpoints'; | ||||||
| import { apiUrl } from '../../states/ApiState'; | import { apiUrl } from '../../states/ApiState'; | ||||||
| 
 | 
 | ||||||
| export default function Set_Password() { | export default function ResetPassword() { | ||||||
|   const simpleForm = useForm({ initialValues: { password: '' } }); |   const simpleForm = useForm({ initialValues: { password: '' } }); | ||||||
|   const [searchParams] = useSearchParams(); |   const [searchParams] = useSearchParams(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
| @@ -3,15 +3,17 @@ import { Group, Stack, Table, Title } from '@mantine/core'; | |||||||
| import { IconKey, IconUser } from '@tabler/icons-react'; | import { IconKey, IconUser } from '@tabler/icons-react'; | ||||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||||
|  |  | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
| import { YesNoUndefinedButton } from '../../../../components/buttons/YesNoButton'; | import { YesNoUndefinedButton } from '../../../../components/buttons/YesNoButton'; | ||||||
| import type { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField'; | import type { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField'; | ||||||
| import { ActionDropdown } from '../../../../components/items/ActionDropdown'; | import { ActionDropdown } from '../../../../components/items/ActionDropdown'; | ||||||
| import { ApiEndpoints } from '../../../../enums/ApiEndpoints'; | import { ApiEndpoints } from '../../../../enums/ApiEndpoints'; | ||||||
| import { notYetImplemented } from '../../../../functions/notifications'; |  | ||||||
| import { useEditApiFormModal } from '../../../../hooks/UseForm'; | import { useEditApiFormModal } from '../../../../hooks/UseForm'; | ||||||
| import { useUserState } from '../../../../states/UserState'; | import { useUserState } from '../../../../states/UserState'; | ||||||
|  |  | ||||||
| export function AccountDetailPanel() { | export function AccountDetailPanel() { | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|   const [user, fetchUserState] = useUserState((state) => [ |   const [user, fetchUserState] = useUserState((state) => [ | ||||||
|     state.user, |     state.user, | ||||||
|     state.fetchUserState |     state.fetchUserState | ||||||
| @@ -51,10 +53,12 @@ export function AccountDetailPanel() { | |||||||
|                 onClick: editUser.open |                 onClick: editUser.open | ||||||
|               }, |               }, | ||||||
|               { |               { | ||||||
|                 name: t`Set Password`, |                 name: t`Change Password`, | ||||||
|                 icon: <IconKey />, |                 icon: <IconKey />, | ||||||
|                 tooltip: t`Set User Password`, |                 tooltip: t`Change User Password`, | ||||||
|                 onClick: notYetImplemented |                 onClick: () => { | ||||||
|  |                   navigate('/change-password'); | ||||||
|  |                 } | ||||||
|               } |               } | ||||||
|             ]} |             ]} | ||||||
|           /> |           /> | ||||||
|   | |||||||
| @@ -107,8 +107,13 @@ export const Login = Loadable(lazy(() => import('./pages/Auth/Login'))); | |||||||
| export const Logout = Loadable(lazy(() => import('./pages/Auth/Logout'))); | export const Logout = Loadable(lazy(() => import('./pages/Auth/Logout'))); | ||||||
| export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In'))); | export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In'))); | ||||||
| export const Reset = Loadable(lazy(() => import('./pages/Auth/Reset'))); | export const Reset = Loadable(lazy(() => import('./pages/Auth/Reset'))); | ||||||
| export const Set_Password = Loadable( |  | ||||||
|   lazy(() => import('./pages/Auth/Set-Password')) | export const ChangePassword = Loadable( | ||||||
|  |   lazy(() => import('./pages/Auth/ChangePassword')) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const ResetPassword = Loadable( | ||||||
|  |   lazy(() => import('./pages/Auth/ResetPassword')) | ||||||
| ); | ); | ||||||
|  |  | ||||||
| // Routes | // Routes | ||||||
| @@ -168,7 +173,8 @@ export const routes = ( | |||||||
|       <Route path='/logout' element={<Logout />} />, |       <Route path='/logout' element={<Logout />} />, | ||||||
|       <Route path='/logged-in' element={<Logged_In />} /> |       <Route path='/logged-in' element={<Logged_In />} /> | ||||||
|       <Route path='/reset-password' element={<Reset />} /> |       <Route path='/reset-password' element={<Reset />} /> | ||||||
|       <Route path='/set-password' element={<Set_Password />} /> |       <Route path='/set-password' element={<ResetPassword />} /> | ||||||
|  |       <Route path='/change-password' element={<ChangePassword />} /> | ||||||
|     </Route> |     </Route> | ||||||
|   </Routes> |   </Routes> | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -116,6 +116,8 @@ test('Parts - Allocations', async ({ page }) => { | |||||||
|   await page.getByText('5 / 109').waitFor(); |   await page.getByText('5 / 109').waitFor(); | ||||||
|  |  | ||||||
|   // Navigate to the "Allocations" tab |   // Navigate to the "Allocations" tab | ||||||
|  |   await page.waitForTimeout(500); | ||||||
|  |  | ||||||
|   await page.getByRole('tab', { name: 'Allocations' }).click(); |   await page.getByRole('tab', { name: 'Allocations' }).click(); | ||||||
|  |  | ||||||
|   await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); |   await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); | ||||||
|   | |||||||
| @@ -95,3 +95,37 @@ test('Login - Failures', async ({ page }) => { | |||||||
|  |  | ||||||
|   await page.waitForTimeout(2500); |   await page.waitForTimeout(2500); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | test('Login - Change Password', async ({ page }) => { | ||||||
|  |   await doQuickLogin(page, 'noaccess', 'youshallnotpass'); | ||||||
|  |  | ||||||
|  |   // Navigate to the 'change password' page | ||||||
|  |   await page.goto(`${baseUrl}/settings/user/account`); | ||||||
|  |   await page.getByLabel('action-menu-user-actions').click(); | ||||||
|  |   await page.getByLabel('action-menu-user-actions-change-password').click(); | ||||||
|  |  | ||||||
|  |   // First attempt with some errors | ||||||
|  |   await page.getByLabel('input-password-1').fill('12345'); | ||||||
|  |   await page.getByLabel('input-password-2').fill('54321'); | ||||||
|  |   await page.getByRole('button', { name: 'Confirm' }).click(); | ||||||
|  |   await page.getByText('The two password fields didn’t match').waitFor(); | ||||||
|  |  | ||||||
|  |   await page.getByLabel('input-password-2').fill('12345'); | ||||||
|  |   await page.getByRole('button', { name: 'Confirm' }).click(); | ||||||
|  |  | ||||||
|  |   await page.getByText('This password is too short').waitFor(); | ||||||
|  |   await page.getByText('This password is entirely numeric').waitFor(); | ||||||
|  |  | ||||||
|  |   await page.getByLabel('input-password-1').fill('youshallnotpass'); | ||||||
|  |   await page.getByLabel('input-password-2').fill('youshallnotpass'); | ||||||
|  |   await page.getByRole('button', { name: 'Confirm' }).click(); | ||||||
|  |  | ||||||
|  |   await page.getByText('Password Changed').waitFor(); | ||||||
|  |   await page.getByText('The password was set successfully').waitFor(); | ||||||
|  |  | ||||||
|  |   // Should have redirected to the index page | ||||||
|  |   await page.waitForURL('**/platform/home**'); | ||||||
|  |   await page.getByText('InvenTree Demo Server - Norman Nothington'); | ||||||
|  |  | ||||||
|  |   await page.waitForTimeout(1000); | ||||||
|  | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user