From 92336f6b32e7b4cf8dc693358d9ebd70326c0a65 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 3 Nov 2023 01:23:45 +0100 Subject: [PATCH] [PUI] Settings simplification and restructure (#5822) * unify file structure for settings * fixed router * clean up menu * refactored user settings mergerd profile and user setttings * removed profile page entirely * cleaned up account panels * use more detailed link * refactored settings page header * fixed user settings save * simplified user data handling * fixed UserState invalidation after form submition * removed username from account change this can currently not be done safely * Added basic security section * Added way to remove SSO account * Only show providers that are not in use * Changed API to contain configuration change * removed unused var * Added email section to PUI * Switched rending to vertical * Added things for adding a new email * removed sessions as we are not using that in PUI * made rendering logic easier to understand * alligned colums horizontally * allign action buttons for email --- InvenTree/InvenTree/api_version.py | 4 + InvenTree/InvenTree/social_auth_urls.py | 93 ++++- InvenTree/InvenTree/urls.py | 10 +- src/frontend/src/components/nav/MainMenu.tsx | 15 +- .../src/components/nav/SettingsHeader.tsx | 41 +++ .../src/pages/Index/Profile/Profile.tsx | 33 -- .../src/pages/Index/Profile/UserPanel.tsx | 160 --------- .../AccountSettings/AccountDetailPanel.tsx | 67 ++++ .../AccountSettings/DisplaySettingsPanel.tsx | 48 +++ .../AccountSettings/SecurityContent.tsx | 319 ++++++++++++++++++ .../Settings/AccountSettings/UserPanel.tsx | 28 ++ .../AccountSettings/UserThemePanel.tsx} | 15 +- .../Index/{ => Settings}/PluginSettings.tsx | 10 +- .../Index/{ => Settings}/SystemSettings.tsx | 21 +- .../Index/{ => Settings}/UserSettings.tsx | 34 +- src/frontend/src/router.tsx | 12 +- src/frontend/src/states/ApiState.tsx | 25 +- 17 files changed, 686 insertions(+), 249 deletions(-) create mode 100644 src/frontend/src/components/nav/SettingsHeader.tsx delete mode 100644 src/frontend/src/pages/Index/Profile/Profile.tsx delete mode 100644 src/frontend/src/pages/Index/Profile/UserPanel.tsx create mode 100644 src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx create mode 100644 src/frontend/src/pages/Index/Settings/AccountSettings/DisplaySettingsPanel.tsx create mode 100644 src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx create mode 100644 src/frontend/src/pages/Index/Settings/AccountSettings/UserPanel.tsx rename src/frontend/src/pages/Index/{Profile/UserTheme.tsx => Settings/AccountSettings/UserThemePanel.tsx} (91%) rename src/frontend/src/pages/Index/{ => Settings}/PluginSettings.tsx (76%) rename src/frontend/src/pages/Index/{ => Settings}/SystemSettings.tsx (89%) rename src/frontend/src/pages/Index/{ => Settings}/UserSettings.tsx (70%) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 92e5034cdb..01e2fda972 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -7,6 +7,10 @@ INVENTREE_API_VERSION = 145 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v146 -> 2023-11-02: https://github.com/inventree/InvenTree/pull/5822 + - Extended SSO Provider endpoint to contain if a provider is configured + - Adds API endpoints for Email Address model + v145 -> 2023-10-30: https://github.com/inventree/InvenTree/pull/5786 - Allow printing labels via POST including printing options in the body diff --git a/InvenTree/InvenTree/social_auth_urls.py b/InvenTree/InvenTree/social_auth_urls.py index fe6a129673..c2755acb9d 100644 --- a/InvenTree/InvenTree/social_auth_urls.py +++ b/InvenTree/InvenTree/social_auth_urls.py @@ -4,17 +4,21 @@ from importlib import import_module from django.urls import include, path, reverse +from allauth.account.models import EmailAddress from allauth.socialaccount import providers from allauth.socialaccount.models import SocialApp from allauth.socialaccount.providers.keycloak.views import \ KeycloakOAuth2Adapter from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter, OAuth2LoginView) -from rest_framework.generics import ListAPIView -from rest_framework.permissions import AllowAny +from drf_spectacular.utils import OpenApiResponse, extend_schema +from rest_framework.exceptions import NotFound +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from common.models import InvenTreeSetting +from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI +from InvenTree.serializers import InvenTreeModelSerializer logger = logging.getLogger('inventree') @@ -96,7 +100,7 @@ for provider in providers.registry.get_list(): social_auth_urlpatterns += provider_urlpatterns -class SocialProviderListView(ListAPIView): +class SocialProviderListView(ListAPI): """List of available social providers.""" permission_classes = (AllowAny,) @@ -109,9 +113,12 @@ class SocialProviderListView(ListAPIView): 'name': provider.name, 'login': request.build_absolute_uri(reverse(f'{provider.id}_api_login')), 'connect': request.build_absolute_uri(reverse(f'{provider.id}_api_connect')), + 'configured': False } try: - provider_data['display_name'] = provider.get_app(request).name + provider_app = provider.get_app(request) + provider_data['display_name'] = provider_app.name + provider_data['configured'] = True except SocialApp.DoesNotExist: provider_data['display_name'] = provider.name @@ -124,3 +131,81 @@ class SocialProviderListView(ListAPIView): 'providers': provider_list } return Response(data) + + +class EmailAddressSerializer(InvenTreeModelSerializer): + """Serializer for the EmailAddress model.""" + + class Meta: + """Meta options for EmailAddressSerializer.""" + + model = EmailAddress + fields = '__all__' + + +class EmptyEmailAddressSerializer(InvenTreeModelSerializer): + """Empty Serializer for the EmailAddress model.""" + + class Meta: + """Meta options for EmailAddressSerializer.""" + + model = EmailAddress + fields = [] + + +class EmailListView(ListCreateAPI): + """List of registered email addresses for current users.""" + permission_classes = (IsAuthenticated,) + serializer_class = EmailAddressSerializer + + def get_queryset(self): + """Only return data for current user.""" + return EmailAddress.objects.filter(user=self.request.user) + + +class EmailActionMixin(CreateAPI): + """Mixin to modify email addresses for current users.""" + serializer_class = EmptyEmailAddressSerializer + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + """Filter queryset for current user.""" + return EmailAddress.objects.filter(user=self.request.user, pk=self.kwargs['pk']).first() + + @extend_schema(responses={200: OpenApiResponse(response=EmailAddressSerializer)}) + def post(self, request, *args, **kwargs): + """Filter item, run action and return data.""" + email = self.get_queryset() + if not email: + raise NotFound + + self.special_action(email, request, *args, **kwargs) + return Response(EmailAddressSerializer(email).data) + + +class EmailVerifyView(EmailActionMixin): + """Re-verify an email for a currently logged in user.""" + + def special_action(self, email, request, *args, **kwargs): + """Send confirmation.""" + if email.verified: + return + email.send_confirmation(request) + + +class EmailPrimaryView(EmailActionMixin): + """Make an email for a currently logged in user primary.""" + + def special_action(self, email, *args, **kwargs): + """Mark email as primary.""" + if email.primary: + return + email.set_as_primary() + + +class EmailRemoveView(EmailActionMixin): + """Remove an email for a currently logged in user.""" + + def special_action(self, email, *args, **kwargs): + """Delete email.""" + email.delete() diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 5c62db8c75..30c6a29e5e 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -38,7 +38,9 @@ from web.urls import urlpatterns as platform_urls from .api import APISearchView, InfoView, NotFoundView, VersionView from .magic_login import GetSimpleLoginView -from .social_auth_urls import SocialProviderListView, social_auth_urlpatterns +from .social_auth_urls import (EmailListView, EmailPrimaryView, + EmailRemoveView, EmailVerifyView, + SocialProviderListView, social_auth_urlpatterns) from .views import (AboutView, AppearanceSelectView, CustomConnectionsView, CustomEmailView, CustomLoginView, CustomPasswordResetFromKeyView, @@ -85,6 +87,12 @@ apipatterns = [ re_path(r'^registration/account-confirm-email/(?P[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'), path('registration/', include('dj_rest_auth.registration.urls')), path('providers/', SocialProviderListView.as_view(), name='social_providers'), + path('emails/', include([path('/', include([ + path('primary/', EmailPrimaryView.as_view(), name='email-primary'), + path('verify/', EmailVerifyView.as_view(), name='email-verify'), + path('remove/', EmailRemoveView().as_view(), name='email-remove'),])), + path('', EmailListView.as_view(), name='email-list') + ])), path('social/', include(social_auth_urlpatterns)), path('social/', SocialAccountListView.as_view(), name='social_account_list'), path('social//disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'), diff --git a/src/frontend/src/components/nav/MainMenu.tsx b/src/frontend/src/components/nav/MainMenu.tsx index ce15ed0619..f17d786c34 100644 --- a/src/frontend/src/components/nav/MainMenu.tsx +++ b/src/frontend/src/components/nav/MainMenu.tsx @@ -5,7 +5,6 @@ import { IconLogout, IconPlugConnected, IconSettings, - IconUserCircle, IconUserCog } from '@tabler/icons-react'; import { Link } from 'react-router-dom'; @@ -13,7 +12,6 @@ import { Link } from 'react-router-dom'; import { doClassicLogout } from '../../functions/auth'; import { InvenTreeStyle } from '../../globalStyle'; import { useUserState } from '../../states/UserState'; -import { PlaceholderPill } from '../items/Placeholder'; export function MainMenu() { const { classes, theme } = InvenTreeStyle(); @@ -36,21 +34,18 @@ export function MainMenu() { - }> - Profile - - Settings - } component={Link} to="/profile/user"> - Account settings - } component={Link} to="/settings/user"> Account settings {userState.user?.is_staff && ( - } component={Link} to="/settings/"> + } + component={Link} + to="/settings/system" + > System Settings )} diff --git a/src/frontend/src/components/nav/SettingsHeader.tsx b/src/frontend/src/components/nav/SettingsHeader.tsx new file mode 100644 index 0000000000..b0542d4d3e --- /dev/null +++ b/src/frontend/src/components/nav/SettingsHeader.tsx @@ -0,0 +1,41 @@ +import { Anchor, Group, Stack, Text, Title } from '@mantine/core'; +import { IconSwitch } from '@tabler/icons-react'; +import { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; + +/** + * Construct a settings page header with interlinks to one other settings page + */ +export function SettingsHeader({ + title, + shorthand, + subtitle, + switch_condition = true, + switch_text, + switch_link +}: { + title: string; + shorthand?: string; + subtitle: string | ReactNode; + switch_condition?: boolean; + switch_text: string | ReactNode; + switch_link: string; +}) { + return ( + + + {title} + {shorthand && ({shorthand})} + + + {subtitle} + {switch_condition && ( + + + {switch_text} + + )} + + + ); +} diff --git a/src/frontend/src/pages/Index/Profile/Profile.tsx b/src/frontend/src/pages/Index/Profile/Profile.tsx deleted file mode 100644 index 90ee801db4..0000000000 --- a/src/frontend/src/pages/Index/Profile/Profile.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Trans } from '@lingui/macro'; -import { Tabs } from '@mantine/core'; -import { useNavigate, useParams } from 'react-router-dom'; - -import { StylishText } from '../../../components/items/StylishText'; -import { UserPanel } from './UserPanel'; - -export default function Profile() { - const navigate = useNavigate(); - const { tabValue } = useParams(); - - return ( - <> - - Profile - - navigate(`/profile/${value}`)} - > - - - User - - - - - - - - - ); -} diff --git a/src/frontend/src/pages/Index/Profile/UserPanel.tsx b/src/frontend/src/pages/Index/Profile/UserPanel.tsx deleted file mode 100644 index 87ae284cf1..0000000000 --- a/src/frontend/src/pages/Index/Profile/UserPanel.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Trans } from '@lingui/macro'; -import { - Button, - Container, - Grid, - Group, - SimpleGrid, - Skeleton, - Stack, - Text, - TextInput, - Title -} from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useToggle } from '@mantine/hooks'; -import { useQuery } from '@tanstack/react-query'; - -import { api, queryClient } from '../../../App'; -import { ColorToggle } from '../../../components/items/ColorToggle'; -import { EditButton } from '../../../components/items/EditButton'; -import { LanguageSelect } from '../../../components/items/LanguageSelect'; -import { ApiPaths, apiUrl } from '../../../states/ApiState'; -import { useLocalState } from '../../../states/LocalState'; -import { UserTheme } from './UserTheme'; - -export function UserPanel() { - // view - const PRIMARY_COL_HEIGHT = 300; - const SECONDARY_COL_HEIGHT = PRIMARY_COL_HEIGHT / 2 - 8; - - // data - function fetchData() { - // TODO: Replace this call with the global user state, perhaps? - return api.get(apiUrl(ApiPaths.user_me)).then((res) => res.data); - } - const { isLoading, data } = useQuery({ - queryKey: ['user-me'], - queryFn: fetchData - }); - - return ( -
- - - {isLoading ? ( - - ) : ( - - )} - - - - - - - - - - - - - -
- ); -} - -export function UserInfo({ data }: { data: any }) { - if (!data) return ; - - const form = useForm({ initialValues: data }); - const [editing, setEditing] = useToggle([false, true] as const); - function SaveData(values: any) { - api.put(apiUrl(ApiPaths.user_me)).then((res) => { - if (res.status === 200) { - setEditing(); - queryClient.invalidateQueries({ queryKey: ['user-me'] }); - } - }); - } - - return ( -
SaveData(values))}> - - - <Trans>Userinfo</Trans> - - - - - {editing ? ( - - - - - - - - - ) : ( - - - First name: {form.values.first_name} - - - Last name: {form.values.last_name} - - - Username: {form.values.username} - - - )} - -
- ); -} - -function DisplaySettings({ height }: { height: number }) { - function enablePseudoLang(): void { - useLocalState.setState({ language: 'pseudo-LOCALE' }); - } - - return ( - - - <Trans>Display Settings</Trans> - - - - Color Mode - - - - - - Language - - - - - - - - ); -} diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx new file mode 100644 index 0000000000..c29df03358 --- /dev/null +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx @@ -0,0 +1,67 @@ +import { Trans } from '@lingui/macro'; +import { Button, Group, Stack, Text, TextInput, Title } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useToggle } from '@mantine/hooks'; + +import { api } from '../../../../App'; +import { EditButton } from '../../../../components/items/EditButton'; +import { ApiPaths, apiUrl } from '../../../../states/ApiState'; +import { useUserState } from '../../../../states/UserState'; + +export function AccountDetailPanel() { + const [user, fetchUserState] = useUserState((state) => [ + state.user, + state.fetchUserState + ]); + const form = useForm({ initialValues: user }); + const [editing, setEditing] = useToggle([false, true] as const); + function SaveData(values: any) { + api.put(apiUrl(ApiPaths.user_me), values).then((res) => { + if (res.status === 200) { + setEditing(); + fetchUserState(); + } + }); + } + + return ( +
SaveData(values))}> + + + <Trans>Account Details</Trans> + + + + + {editing ? ( + + + + + + + + ) : ( + + + First name: {form.values.first_name} + + + Last name: {form.values.last_name} + + + )} + +
+ ); +} diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/DisplaySettingsPanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/DisplaySettingsPanel.tsx new file mode 100644 index 0000000000..7194b63c0b --- /dev/null +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/DisplaySettingsPanel.tsx @@ -0,0 +1,48 @@ +import { Trans } from '@lingui/macro'; +import { Button, Container, Group, Table, Title } from '@mantine/core'; + +import { ColorToggle } from '../../../../components/items/ColorToggle'; +import { LanguageSelect } from '../../../../components/items/LanguageSelect'; +import { useLocalState } from '../../../../states/LocalState'; + +export function DisplaySettingsPanel({ height }: { height: number }) { + function enablePseudoLang(): void { + useLocalState.setState({ language: 'pseudo-LOCALE' }); + } + + return ( + + + <Trans>Display Settings</Trans> + + + + + + + + + + + + +
+ Color Mode + + + + +
+ Language + + {' '} + + + + +
+
+ ); +} diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx new file mode 100644 index 0000000000..ddf52400c3 --- /dev/null +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx @@ -0,0 +1,319 @@ +import { Trans, t } from '@lingui/macro'; +import { + Alert, + Badge, + Button, + Grid, + Group, + Loader, + Radio, + Stack, + Text, + TextInput, + Title, + Tooltip +} from '@mantine/core'; +import { IconAlertCircle, IconAt } from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; + +import { api, queryClient } from '../../../../App'; +import { PlaceholderPill } from '../../../../components/items/Placeholder'; +import { ApiPaths, apiUrl } from '../../../../states/ApiState'; + +export function SecurityContent() { + const [isSsoEnabled, setIsSsoEnabled] = useState(false); + const [isMfaEnabled, setIsMfaEnabled] = useState(false); + + const { isLoading: isLoadingProvider, data: dataProvider } = useQuery({ + queryKey: ['sso-providers'], + queryFn: () => + api.get(apiUrl(ApiPaths.sso_providers)).then((res) => res.data) + }); + + // evaluate if security options are enabled + useEffect(() => { + if (dataProvider === undefined) return; + + // check if SSO is enabled on the server + setIsSsoEnabled(dataProvider.sso_enabled || false); + // check if MFa is enabled + setIsMfaEnabled(dataProvider.mfa_required || false); + }, [dataProvider]); + + return ( + + + <Trans>Email</Trans> + + + + <Trans>Single Sign On Accounts</Trans> + + {isSsoEnabled ? ( + + ) : ( + } + title={t`Not enabled`} + color="yellow" + > + Single Sign On is not enabled for this server + + )} + + <Trans>Multifactor</Trans> + + {isLoadingProvider ? ( + + ) : ( + <> + {isMfaEnabled ? ( + + ) : ( + } + title={t`Not enabled`} + color="yellow" + > + + Multifactor authentication is not configured for your account{' '} + + + )} + + )} + + ); +} + +function EmailContent({}: {}) { + const [value, setValue] = useState(''); + const [newEmailValue, setNewEmailValue] = useState(''); + const { isLoading, data, refetch } = useQuery({ + queryKey: ['emails'], + queryFn: () => api.get(apiUrl(ApiPaths.user_emails)).then((res) => res.data) + }); + + function runServerAction(url: ApiPaths) { + api + .post(apiUrl(url).replace('$id', value), {}) + .then(() => { + refetch(); + }) + .catch((res) => console.log(res.data)); + } + + function addEmail() { + api + .post(apiUrl(ApiPaths.user_emails), { + email: newEmailValue + }) + .then(() => { + refetch(); + }) + .catch((res) => console.log(res.data)); + } + + if (isLoading) return ; + + return ( + + + + + {data.map((link: any) => ( + + {link.email} + {link.primary && ( + + Primary + + )} + {link.verified ? ( + + Verified + + ) : ( + + Unverified + + )} + + } + /> + ))} + + + + + + + Add Email Address + + } + value={newEmailValue} + onChange={(event) => setNewEmailValue(event.currentTarget.value)} + /> + + + + + + + + + + + + + + ); +} + +function SsoContent({ dataProvider }: { dataProvider: any | undefined }) { + const [value, setValue] = useState(''); + const [currentProviders, setcurrentProviders] = useState<[]>(); + const { isLoading, data } = useQuery({ + queryKey: ['sso-list'], + queryFn: () => api.get(apiUrl(ApiPaths.user_sso)).then((res) => res.data) + }); + + useEffect(() => { + if (dataProvider === undefined) return; + if (data === undefined) return; + + const configuredProviders = data.map((item: any) => { + return item.provider; + }); + function isAlreadyInUse(value: any) { + return !configuredProviders.includes(value.id); + } + + // remove providers that are used currently + let newData = dataProvider.providers; + newData = newData.filter(isAlreadyInUse); + setcurrentProviders(newData); + }, [dataProvider, data]); + + function removeProvider() { + api + .post(apiUrl(ApiPaths.user_sso_remove).replace('$id', value)) + .then(() => { + queryClient.removeQueries({ + queryKey: ['sso-list'] + }); + }) + .catch((res) => console.log(res.data)); + } + + /* renderer */ + if (isLoading) return ; + + function ProviderButton({ provider }: { provider: any }) { + const button = ( + + ); + + if (provider.configured) return button; + return ( + {button} + ); + } + + return ( + + + {data.length == 0 ? ( + } + title={t`Not configured`} + color="yellow" + > + + There are no social network accounts connected to this account.{' '} + + + ) : ( + + + + {data.map((link: any) => ( + + ))} + + + + + )} + + + + Add SSO Account + + {currentProviders === undefined ? ( + Loading + ) : ( + + {currentProviders.map((provider: any) => ( + + ))} + + )} + + + + + ); +} + +function MfaContent({}: {}) { + return ( + <> + MFA Details + + + ); +} diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/UserPanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/UserPanel.tsx new file mode 100644 index 0000000000..a35e998f47 --- /dev/null +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/UserPanel.tsx @@ -0,0 +1,28 @@ +import { Container, Grid, SimpleGrid } from '@mantine/core'; + +import { AccountDetailPanel } from './AccountDetailPanel'; +import { DisplaySettingsPanel } from './DisplaySettingsPanel'; +import { UserTheme } from './UserThemePanel'; + +export function AccountContent() { + const PRIMARY_COL_HEIGHT = 300; + const SECONDARY_COL_HEIGHT = PRIMARY_COL_HEIGHT / 2 - 8; + + return ( +
+ + + + + + + + + + + + + +
+ ); +} diff --git a/src/frontend/src/pages/Index/Profile/UserTheme.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx similarity index 91% rename from src/frontend/src/pages/Index/Profile/UserTheme.tsx rename to src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx index 4694146f6a..e6c62d1698 100644 --- a/src/frontend/src/pages/Index/Profile/UserTheme.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx @@ -8,17 +8,15 @@ import { Loader, Select, Slider, - Space, Table, Title } from '@mantine/core'; import { LoaderType } from '@mantine/styles/lib/theme/types/MantineTheme'; import { useState } from 'react'; -import { PlaceholderPill } from '../../../components/items/Placeholder'; -import { SizeMarks } from '../../../defaults/defaults'; -import { InvenTreeStyle } from '../../../globalStyle'; -import { useLocalState } from '../../../states/LocalState'; +import { SizeMarks } from '../../../../defaults/defaults'; +import { InvenTreeStyle } from '../../../../globalStyle'; +import { useLocalState } from '../../../../states/LocalState'; function getLkp(color: string) { return { [DEFAULT_THEME.colors[color][6]]: color }; @@ -80,9 +78,7 @@ export function UserTheme({ height }: { height: number }) { return ( - <Trans> - Design <PlaceholderPill /> - </Trans> + <Trans>Theme</Trans> @@ -137,13 +133,12 @@ export function UserTheme({ height }: { height: number }) { diff --git a/src/frontend/src/pages/Index/PluginSettings.tsx b/src/frontend/src/pages/Index/Settings/PluginSettings.tsx similarity index 76% rename from src/frontend/src/pages/Index/PluginSettings.tsx rename to src/frontend/src/pages/Index/Settings/PluginSettings.tsx index 84c325b2ee..9706686770 100644 --- a/src/frontend/src/pages/Index/PluginSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/PluginSettings.tsx @@ -3,11 +3,11 @@ import { LoadingOverlay, Stack } from '@mantine/core'; import { IconPlugConnected } from '@tabler/icons-react'; import { useMemo } from 'react'; -import { PageDetail } from '../../components/nav/PageDetail'; -import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; -import { PluginListTable } from '../../components/tables/plugin/PluginListTable'; -import { useInstance } from '../../hooks/UseInstance'; -import { ApiPaths } from '../../states/ApiState'; +import { PageDetail } from '../../../components/nav/PageDetail'; +import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; +import { PluginListTable } from '../../../components/tables/plugin/PluginListTable'; +import { useInstance } from '../../../hooks/UseInstance'; +import { ApiPaths } from '../../../states/ApiState'; /** * Plugins settings page diff --git a/src/frontend/src/pages/Index/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx similarity index 89% rename from src/frontend/src/pages/Index/SystemSettings.tsx rename to src/frontend/src/pages/Index/Settings/SystemSettings.tsx index 4428e63592..61f1fd127f 100644 --- a/src/frontend/src/pages/Index/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -1,4 +1,4 @@ -import { t } from '@lingui/macro'; +import { Trans, t } from '@lingui/macro'; import { Divider, Stack } from '@mantine/core'; import { IconBellCog, @@ -21,11 +21,12 @@ import { } from '@tabler/icons-react'; import { useMemo } from 'react'; -import { PageDetail } from '../../components/nav/PageDetail'; -import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; -import { GlobalSettingList } from '../../components/settings/SettingList'; -import { CustomUnitsTable } from '../../components/tables/settings/CustomUnitsTable'; -import { ProjectCodeTable } from '../../components/tables/settings/ProjectCodeTable'; +import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; +import { SettingsHeader } from '../../../components/nav/SettingsHeader'; +import { GlobalSettingList } from '../../../components/settings/SettingList'; +import { CustomUnitsTable } from '../../../components/tables/settings/CustomUnitsTable'; +import { ProjectCodeTable } from '../../../components/tables/settings/ProjectCodeTable'; +import { useServerApiState } from '../../../states/ApiState'; /** * System settings page @@ -249,11 +250,17 @@ export default function SystemSettings() { } ]; }, []); + const [server] = useServerApiState((state) => [state.server]); return ( <> - + System Settings} + switch_link="/settings/user" + switch_text={Switch to User Setting} + /> diff --git a/src/frontend/src/pages/Index/UserSettings.tsx b/src/frontend/src/pages/Index/Settings/UserSettings.tsx similarity index 70% rename from src/frontend/src/pages/Index/UserSettings.tsx rename to src/frontend/src/pages/Index/Settings/UserSettings.tsx index 5042f24a3d..232fd1d7e8 100644 --- a/src/frontend/src/pages/Index/UserSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/UserSettings.tsx @@ -1,18 +1,22 @@ -import { t } from '@lingui/macro'; -import { Stack, Text } from '@mantine/core'; +import { Trans, t } from '@lingui/macro'; +import { Stack } from '@mantine/core'; import { IconBellCog, IconDeviceDesktop, IconDeviceDesktopAnalytics, IconFileAnalytics, + IconLock, IconSearch, IconUserCircle } from '@tabler/icons-react'; import { useMemo } from 'react'; -import { PageDetail } from '../../components/nav/PageDetail'; -import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; -import { UserSettingList } from '../../components/settings/SettingList'; +import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; +import { SettingsHeader } from '../../../components/nav/SettingsHeader'; +import { UserSettingList } from '../../../components/settings/SettingList'; +import { useUserState } from '../../../states/UserState'; +import { SecurityContent } from './AccountSettings/SecurityContent'; +import { AccountContent } from './AccountSettings/UserPanel'; /** * User settings page @@ -23,7 +27,14 @@ export default function UserSettings() { { name: 'account', label: t`Account`, - icon: + icon: , + content: + }, + { + name: 'security', + label: t`Security`, + icon: , + content: }, { name: 'dashboard', @@ -95,13 +106,18 @@ export default function UserSettings() { } ]; }, []); + const [user] = useUserState((state) => [state.user]); return ( <> - TODO: Filler} + Account Settings} + switch_link="/settings/system" + switch_text={Switch to System Setting} + switch_condition={user?.is_staff || false} /> diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index d044882ec7..d432ed5ea0 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -81,20 +81,16 @@ export const Notifications = Loadable( lazy(() => import('./pages/Notifications')) ); -export const Profile = Loadable( - lazy(() => import('./pages/Index/Profile/Profile')) -); - export const UserSettings = Loadable( - lazy(() => import('./pages/Index/UserSettings')) + lazy(() => import('./pages/Index/Settings/UserSettings')) ); export const SystemSettings = Loadable( - lazy(() => import('./pages/Index/SystemSettings')) + lazy(() => import('./pages/Index/Settings/SystemSettings')) ); export const PluginSettings = Loadable( - lazy(() => import('./pages/Index/PluginSettings')) + lazy(() => import('./pages/Index/Settings/PluginSettings')) ); export const NotFound = Loadable(lazy(() => import('./pages/NotFound'))); @@ -118,6 +114,7 @@ export const routes = ( } />, } /> + } /> } /> } /> @@ -148,7 +145,6 @@ export const routes = ( } /> } /> - } /> }> } />, diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index eb77dbc10b..8b54433277 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -60,6 +60,12 @@ export enum ApiPaths { user_simple_login = 'api-user-simple-login', user_reset = 'api-user-reset', user_reset_set = 'api-user-reset-set', + user_sso = 'api-user-sso', + user_sso_remove = 'api-user-sso-remove', + user_emails = 'api-user-emails', + user_email_verify = 'api-user-email-verify', + user_email_primary = 'api-user-email-primary', + user_email_remove = 'api-user-email-remove', settings_global_list = 'api-settings-global-list', settings_user_list = 'api-settings-user-list', @@ -69,6 +75,7 @@ export enum ApiPaths { news = 'news', global_status = 'api-global-status', version = 'api-version', + sso_providers = 'api-sso-providers', // Build order URLs build_order_list = 'api-build-list', @@ -141,10 +148,22 @@ export function apiEndpoint(path: ApiPaths): string { return 'email/generate/'; case ApiPaths.user_reset: // Note leading prefix here - return '/auth/password/reset/'; + return 'auth/password/reset/'; case ApiPaths.user_reset_set: // Note leading prefix here - return '/auth/password/reset/confirm/'; + return 'auth/password/reset/confirm/'; + case ApiPaths.user_sso: + return 'auth/social/'; + case ApiPaths.user_sso_remove: + return 'auth/social/$id/disconnect/'; + case ApiPaths.user_emails: + return 'auth/emails/'; + case ApiPaths.user_email_remove: + return 'auth/emails/$id/remove/'; + case ApiPaths.user_email_verify: + return 'auth/emails/$id/verify/'; + case ApiPaths.user_email_primary: + return 'auth/emails/$id/primary/'; case ApiPaths.api_search: return 'search/'; case ApiPaths.settings_global_list: @@ -161,6 +180,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'generic/status/'; case ApiPaths.version: return 'version/'; + case ApiPaths.sso_providers: + return 'auth/providers/'; case ApiPaths.build_order_list: return 'build/'; case ApiPaths.build_order_attachment_list:
- -