From 624121ea2985065178ac0c31204213fe6b2b712d Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 6 Nov 2023 01:18:58 +0100 Subject: [PATCH] Added first UI components for user managment Ref #4962 --- InvenTree/InvenTree/serializers.py | 34 +++++ InvenTree/users/api.py | 15 +-- .../components/tables/settings/GroupTable.tsx | 95 ++++++++++++++ .../components/tables/settings/UserTable.tsx | 118 ++++++++++++++++++ .../src/pages/Index/Settings/AdminCenter.tsx | 11 +- src/frontend/src/states/ApiState.tsx | 11 ++ 6 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 src/frontend/src/components/tables/settings/GroupTable.tsx create mode 100644 src/frontend/src/components/tables/settings/UserTable.tsx diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 221463df06..33f2981bc2 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -6,6 +6,7 @@ from decimal import Decimal from django.conf import settings from django.contrib.auth.models import User +from django.contrib.sites.models import Site from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -302,6 +303,39 @@ class UserSerializer(InvenTreeModelSerializer): ] +class UserCreateSerializer(UserSerializer): + """Serializer for creating a new User.""" + class Meta(UserSerializer.Meta): + """Metaclass defines serializer fields.""" + read_only_fields = [] + + def validate(self, attrs): + """Expanded valiadation for auth.""" + # Check that the user trying to create a new user is a superuser + if not self.context['request'].user.is_superuser: + raise serializers.ValidationError(_("Only superusers can create new users")) + + # Generate a random password + password = User.objects.make_random_password(length=14) + attrs.update({'password': password}) + return super().validate(attrs) + + def create(self, validated_data): + """Send an e email to the user after creation.""" + instance = super().create(validated_data) + + # Make sure the user cannot login until they have set a password + instance.set_unusable_password() + # Send the user an onboarding email (from current site) + current_site = Site.objects.get_current() + domain = current_site.domain + instance.email_user( + subject=_(f"Welcome to {current_site.name}"), + message=_(f"Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain})."), + ) + return instance + + class InvenTreeAttachmentSerializerField(serializers.FileField): """Override the DRF native FileField serializer, to remove the leading server path. diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py index 114cc6a277..60b2bd7eb3 100644 --- a/InvenTree/users/api.py +++ b/InvenTree/users/api.py @@ -12,8 +12,9 @@ from rest_framework.response import Response from rest_framework.views import APIView from InvenTree.filters import InvenTreeSearchFilter -from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateAPI -from InvenTree.serializers import UserSerializer +from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI, + RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) +from InvenTree.serializers import UserCreateSerializer, UserSerializer from users.models import ApiToken, Owner, RuleSet, check_user_role from users.serializers import GroupSerializer, OwnerSerializer @@ -112,7 +113,7 @@ class RoleDetails(APIView): return Response(data) -class UserDetail(RetrieveAPI): +class UserDetail(RetrieveUpdateDestroyAPI): """Detail endpoint for a single user.""" queryset = User.objects.all() @@ -130,11 +131,11 @@ class MeUserDetail(RetrieveUpdateAPI, UserDetail): return self.request.user -class UserList(ListAPI): +class UserList(ListCreateAPI): """List endpoint for detail on all users.""" queryset = User.objects.all() - serializer_class = UserSerializer + serializer_class = UserCreateSerializer permission_classes = [ permissions.IsAuthenticated, ] @@ -151,7 +152,7 @@ class UserList(ListAPI): ] -class GroupDetail(RetrieveAPI): +class GroupDetail(RetrieveUpdateDestroyAPI): """Detail endpoint for a particular auth group""" queryset = Group.objects.all() @@ -161,7 +162,7 @@ class GroupDetail(RetrieveAPI): ] -class GroupList(ListAPI): +class GroupList(ListCreateAPI): """List endpoint for all auth groups""" queryset = Group.objects.all() diff --git a/src/frontend/src/components/tables/settings/GroupTable.tsx b/src/frontend/src/components/tables/settings/GroupTable.tsx new file mode 100644 index 0000000000..bcdbffd0ea --- /dev/null +++ b/src/frontend/src/components/tables/settings/GroupTable.tsx @@ -0,0 +1,95 @@ +import { t } from '@lingui/macro'; +import { Text } from '@mantine/core'; +import { useCallback, useMemo } from 'react'; + +import { + openCreateApiForm, + openDeleteApiForm, + openEditApiForm +} from '../../../functions/forms'; +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { AddItemButton } from '../../buttons/AddItemButton'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions'; + +/** + * Table for displaying list of groups + */ +export function GroupTable() { + const { tableKey, refreshTable } = useTableRefresh('groups'); + + const columns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'name', + sortable: true, + title: t`Name` + } + ]; + }, []); + + const rowActions = useCallback((record: any): RowAction[] => { + return [ + RowEditAction({ + onClick: () => { + openEditApiForm({ + url: ApiPaths.group_list, + pk: record.pk, + title: t`Edit group`, + fields: { + name: {} + }, + onFormSuccess: refreshTable, + successMessage: t`Group updated` + }); + } + }), + RowDeleteAction({ + onClick: () => { + openDeleteApiForm({ + url: ApiPaths.group_list, + pk: record.pk, + title: t`Delete group`, + successMessage: t`Group deleted`, + onFormSuccess: refreshTable, + preFormContent: ( + {t`Are you sure you want to delete this group?`} + ) + }); + } + }) + ]; + }, []); + + const addGroup = useCallback(() => { + openCreateApiForm({ + url: ApiPaths.group_list, + title: t`Add group`, + fields: { name: {} }, + onFormSuccess: refreshTable, + successMessage: t`Added group` + }); + }, []); + + const tableActions = useMemo(() => { + let actions = []; + + actions.push(); + + return actions; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/settings/UserTable.tsx b/src/frontend/src/components/tables/settings/UserTable.tsx new file mode 100644 index 0000000000..d10a36fcf2 --- /dev/null +++ b/src/frontend/src/components/tables/settings/UserTable.tsx @@ -0,0 +1,118 @@ +import { t } from '@lingui/macro'; +import { Text } from '@mantine/core'; +import { useCallback, useMemo } from 'react'; + +import { + openCreateApiForm, + openDeleteApiForm, + openEditApiForm +} from '../../../functions/forms'; +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { AddItemButton } from '../../buttons/AddItemButton'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions'; + +/** + * Table for displaying list of users + */ +export function UserTable() { + const { tableKey, refreshTable } = useTableRefresh('users'); + + const columns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'email', + sortable: true, + title: t`Email` + }, + { + accessor: 'username', + sortable: true, + switchable: false, + title: t`Username` + }, + { + accessor: 'first_name', + sortable: true, + title: t`First Name` + }, + { + accessor: 'last_name', + sortable: true, + title: t`Last Name` + } + ]; + }, []); + + const rowActions = useCallback((record: any): RowAction[] => { + return [ + RowEditAction({ + onClick: () => { + openEditApiForm({ + url: ApiPaths.user_list, + pk: record.pk, + title: t`Edit user`, + fields: { + email: {}, + first_name: {}, + last_name: {} + }, + onFormSuccess: refreshTable, + successMessage: t`User updated` + }); + } + }), + RowDeleteAction({ + onClick: () => { + openDeleteApiForm({ + url: ApiPaths.user_list, + pk: record.pk, + title: t`Delete user`, + successMessage: t`user deleted`, + onFormSuccess: refreshTable, + preFormContent: ( + {t`Are you sure you want to delete this user?`} + ) + }); + } + }) + ]; + }, []); + + const addUser = useCallback(() => { + openCreateApiForm({ + url: ApiPaths.user_list, + title: t`Add user`, + fields: { + username: {}, + email: {}, + first_name: {}, + last_name: {} + }, + onFormSuccess: refreshTable, + successMessage: t`Added user` + }); + }, []); + + const tableActions = useMemo(() => { + let actions = []; + + actions.push(); + + return actions; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter.tsx index 681ceea6e6..9596043de6 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter.tsx @@ -16,6 +16,8 @@ import { PlaceholderPill } from '../../../components/items/Placeholder'; import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; import { SettingsHeader } from '../../../components/nav/SettingsHeader'; import { GlobalSettingList } from '../../../components/settings/SettingList'; +import { GroupTable } from '../../../components/tables/settings/GroupTable'; +import { UserTable } from '../../../components/tables/settings/UserTable'; /** * System settings page @@ -28,7 +30,14 @@ export default function AdminCenter() { label: t`User Management`, content: ( - + + <Trans>Users</Trans> + + + + <Trans>Groups</Trans> + + diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 186b55dafe..68d9bd5051 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -80,6 +80,11 @@ export enum ApiPaths { version = 'api-version', sso_providers = 'api-sso-providers', + // User management + user_list = 'api-user-list', + group_list = 'api-group-list', + owner_list = 'api-owner-list', + // Build order URLs build_order_list = 'api-build-list', build_order_attachment_list = 'api-build-attachment-list', @@ -192,6 +197,12 @@ export function apiEndpoint(path: ApiPaths): string { return 'version/'; case ApiPaths.sso_providers: return 'auth/providers/'; + case ApiPaths.user_list: + return 'user/'; + case ApiPaths.group_list: + return 'user/group/'; + case ApiPaths.owner_list: + return 'user/owner/'; case ApiPaths.build_order_list: return 'build/'; case ApiPaths.build_order_attachment_list: