diff --git a/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py b/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py
index 04d757c790..a31606b374 100644
--- a/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py
+++ b/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py
@@ -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 ''},
})
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 0206dcd55f..8c0ec8fe38 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -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/',
diff --git a/src/frontend/src/pages/Auth/ChangePassword.tsx b/src/frontend/src/pages/Auth/ChangePassword.tsx
new file mode 100644
index 0000000000..6b6a2b96cd
--- /dev/null
+++ b/src/frontend/src/pages/Auth/ChangePassword.tsx
@@ -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 (
+
+
+
+
+
+ {t`Reset Password`}
+
+ {user.username() && (
+
+
+ {t`User`}
+ {user.username()}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/frontend/src/pages/Auth/Set-Password.tsx b/src/frontend/src/pages/Auth/ResetPassword.tsx
similarity index 98%
rename from src/frontend/src/pages/Auth/Set-Password.tsx
rename to src/frontend/src/pages/Auth/ResetPassword.tsx
index 31f0b4aa19..ac1d5a1362 100644
--- a/src/frontend/src/pages/Auth/Set-Password.tsx
+++ b/src/frontend/src/pages/Auth/ResetPassword.tsx
@@ -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();
diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx
index fcbda4e262..a124a0ac14 100644
--- a/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx
+++ b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx
@@ -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: ,
- tooltip: t`Set User Password`,
- onClick: notYetImplemented
+ tooltip: t`Change User Password`,
+ onClick: () => {
+ navigate('/change-password');
+ }
}
]}
/>
diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx
index c444f9afeb..1eff80d49d 100644
--- a/src/frontend/src/router.tsx
+++ b/src/frontend/src/router.tsx
@@ -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 = (
} />,
} />
} />
- } />
+ } />
+ } />
);
diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts
index 9f19892f03..fc9fbd12cd 100644
--- a/src/frontend/tests/pages/pui_part.spec.ts
+++ b/src/frontend/tests/pages/pui_part.spec.ts
@@ -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();
diff --git a/src/frontend/tests/pui_login.spec.ts b/src/frontend/tests/pui_login.spec.ts
index 90331a9ef5..11add31459 100644
--- a/src/frontend/tests/pui_login.spec.ts
+++ b/src/frontend/tests/pui_login.spec.ts
@@ -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);
+});