mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05: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', | ||||
|                 'title': 'Dynamic Panel', | ||||
|                 'source': self.plugin_static_file('sample_panel.js'), | ||||
|                 'icon': 'part', | ||||
|                 'icon': 'ti:wave-saw-tool:outline', | ||||
|                 'context': { | ||||
|                     'version': INVENTREE_SW_VERSION, | ||||
|                     'plugin_version': self.VERSION, | ||||
| @@ -97,7 +97,7 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug | ||||
|                 'key': 'part-panel', | ||||
|                 'title': _('Part Panel'), | ||||
|                 'source': self.plugin_static_file('sample_panel.js:renderPartPanel'), | ||||
|                 'icon': 'part', | ||||
|                 'icon': 'ti:package_outline', | ||||
|                 'context': {'part_name': part.name if part else ''}, | ||||
|             }) | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,7 @@ export enum ApiEndpoints { | ||||
|   user_simple_login = 'email/generate/', | ||||
|   user_reset = 'auth/password/reset/', | ||||
|   user_reset_set = 'auth/password/reset/confirm/', | ||||
|   user_change_password = 'auth/password/change/', | ||||
|   user_sso = 'auth/social/', | ||||
|   user_sso_remove = 'auth/social/:id/disconnect/', | ||||
|   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 { apiUrl } from '../../states/ApiState'; | ||||
| 
 | ||||
| export default function Set_Password() { | ||||
| export default function ResetPassword() { | ||||
|   const simpleForm = useForm({ initialValues: { password: '' } }); | ||||
|   const [searchParams] = useSearchParams(); | ||||
|   const navigate = useNavigate(); | ||||
| @@ -3,15 +3,17 @@ import { Group, Stack, Table, Title } from '@mantine/core'; | ||||
| import { IconKey, IconUser } from '@tabler/icons-react'; | ||||
| import { useMemo } from 'react'; | ||||
|  | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { YesNoUndefinedButton } from '../../../../components/buttons/YesNoButton'; | ||||
| import type { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField'; | ||||
| import { ActionDropdown } from '../../../../components/items/ActionDropdown'; | ||||
| import { ApiEndpoints } from '../../../../enums/ApiEndpoints'; | ||||
| import { notYetImplemented } from '../../../../functions/notifications'; | ||||
| import { useEditApiFormModal } from '../../../../hooks/UseForm'; | ||||
| import { useUserState } from '../../../../states/UserState'; | ||||
|  | ||||
| export function AccountDetailPanel() { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [user, fetchUserState] = useUserState((state) => [ | ||||
|     state.user, | ||||
|     state.fetchUserState | ||||
| @@ -51,10 +53,12 @@ export function AccountDetailPanel() { | ||||
|                 onClick: editUser.open | ||||
|               }, | ||||
|               { | ||||
|                 name: t`Set Password`, | ||||
|                 name: t`Change Password`, | ||||
|                 icon: <IconKey />, | ||||
|                 tooltip: t`Set User Password`, | ||||
|                 onClick: notYetImplemented | ||||
|                 tooltip: t`Change User Password`, | ||||
|                 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 Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In'))); | ||||
| 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 | ||||
| @@ -168,7 +173,8 @@ export const routes = ( | ||||
|       <Route path='/logout' element={<Logout />} />, | ||||
|       <Route path='/logged-in' element={<Logged_In />} /> | ||||
|       <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> | ||||
|   </Routes> | ||||
| ); | ||||
|   | ||||
| @@ -116,6 +116,8 @@ test('Parts - Allocations', async ({ page }) => { | ||||
|   await page.getByText('5 / 109').waitFor(); | ||||
|  | ||||
|   // Navigate to the "Allocations" tab | ||||
|   await page.waitForTimeout(500); | ||||
|  | ||||
|   await page.getByRole('tab', { name: 'Allocations' }).click(); | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); | ||||
|   | ||||
| @@ -95,3 +95,37 @@ test('Login - Failures', async ({ page }) => { | ||||
|  | ||||
|   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