Added first UI components for user managment

Ref #4962
This commit is contained in:
Matthias Mair 2023-11-06 01:18:58 +01:00
parent ea249c1dc5
commit 624121ea29
No known key found for this signature in database
GPG Key ID: A593429DDA23B66A
6 changed files with 276 additions and 8 deletions

View File

@ -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.

View File

@ -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()

View File

@ -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: (
<Text>{t`Are you sure you want to delete this group?`}</Text>
)
});
}
})
];
}, []);
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(<AddItemButton onClick={addGroup} tooltip={t`Add group`} />);
return actions;
}, []);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.group_list)}
tableKey={tableKey}
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions
}}
/>
);
}

View File

@ -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: (
<Text>{t`Are you sure you want to delete this user?`}</Text>
)
});
}
})
];
}, []);
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(<AddItemButton onClick={addUser} tooltip={t`Add user`} />);
return actions;
}, []);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.user_list)}
tableKey={tableKey}
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions
}}
/>
);
}

View File

@ -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: (
<Stack spacing="xs">
<PlaceholderPill />
<Title order={5}>
<Trans>Users</Trans>
</Title>
<UserTable />
<Title order={5}>
<Trans>Groups</Trans>
</Title>
<GroupTable />
<Divider />
<Stack spacing={0}>
<Text>

View File

@ -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: