mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
parent
ea249c1dc5
commit
624121ea29
@ -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.
|
||||
|
||||
|
@ -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()
|
||||
|
95
src/frontend/src/components/tables/settings/GroupTable.tsx
Normal file
95
src/frontend/src/components/tables/settings/GroupTable.tsx
Normal 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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
118
src/frontend/src/components/tables/settings/UserTable.tsx
Normal file
118
src/frontend/src/components/tables/settings/UserTable.tsx
Normal 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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user