mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 19:45:46 +00:00
@ -6,6 +6,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
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):
|
class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||||
"""Override the DRF native FileField serializer, to remove the leading server path.
|
"""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 rest_framework.views import APIView
|
||||||
|
|
||||||
from InvenTree.filters import InvenTreeSearchFilter
|
from InvenTree.filters import InvenTreeSearchFilter
|
||||||
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateAPI
|
from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI,
|
||||||
from InvenTree.serializers import UserSerializer
|
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||||
|
from InvenTree.serializers import UserCreateSerializer, UserSerializer
|
||||||
from users.models import ApiToken, Owner, RuleSet, check_user_role
|
from users.models import ApiToken, Owner, RuleSet, check_user_role
|
||||||
from users.serializers import GroupSerializer, OwnerSerializer
|
from users.serializers import GroupSerializer, OwnerSerializer
|
||||||
|
|
||||||
@ -112,7 +113,7 @@ class RoleDetails(APIView):
|
|||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
class UserDetail(RetrieveAPI):
|
class UserDetail(RetrieveUpdateDestroyAPI):
|
||||||
"""Detail endpoint for a single user."""
|
"""Detail endpoint for a single user."""
|
||||||
|
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
@ -130,11 +131,11 @@ class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
|||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
class UserList(ListAPI):
|
class UserList(ListCreateAPI):
|
||||||
"""List endpoint for detail on all users."""
|
"""List endpoint for detail on all users."""
|
||||||
|
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserCreateSerializer
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.IsAuthenticated,
|
permissions.IsAuthenticated,
|
||||||
]
|
]
|
||||||
@ -151,7 +152,7 @@ class UserList(ListAPI):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class GroupDetail(RetrieveAPI):
|
class GroupDetail(RetrieveUpdateDestroyAPI):
|
||||||
"""Detail endpoint for a particular auth group"""
|
"""Detail endpoint for a particular auth group"""
|
||||||
|
|
||||||
queryset = Group.objects.all()
|
queryset = Group.objects.all()
|
||||||
@ -161,7 +162,7 @@ class GroupDetail(RetrieveAPI):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class GroupList(ListAPI):
|
class GroupList(ListCreateAPI):
|
||||||
"""List endpoint for all auth groups"""
|
"""List endpoint for all auth groups"""
|
||||||
|
|
||||||
queryset = Group.objects.all()
|
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 { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||||
import { GlobalSettingList } from '../../../components/settings/SettingList';
|
import { GlobalSettingList } from '../../../components/settings/SettingList';
|
||||||
|
import { GroupTable } from '../../../components/tables/settings/GroupTable';
|
||||||
|
import { UserTable } from '../../../components/tables/settings/UserTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System settings page
|
* System settings page
|
||||||
@ -28,7 +30,14 @@ export default function AdminCenter() {
|
|||||||
label: t`User Management`,
|
label: t`User Management`,
|
||||||
content: (
|
content: (
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<PlaceholderPill />
|
<Title order={5}>
|
||||||
|
<Trans>Users</Trans>
|
||||||
|
</Title>
|
||||||
|
<UserTable />
|
||||||
|
<Title order={5}>
|
||||||
|
<Trans>Groups</Trans>
|
||||||
|
</Title>
|
||||||
|
<GroupTable />
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
<Text>
|
<Text>
|
||||||
|
@ -80,6 +80,11 @@ export enum ApiPaths {
|
|||||||
version = 'api-version',
|
version = 'api-version',
|
||||||
sso_providers = 'api-sso-providers',
|
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 URLs
|
||||||
build_order_list = 'api-build-list',
|
build_order_list = 'api-build-list',
|
||||||
build_order_attachment_list = 'api-build-attachment-list',
|
build_order_attachment_list = 'api-build-attachment-list',
|
||||||
@ -192,6 +197,12 @@ export function apiEndpoint(path: ApiPaths): string {
|
|||||||
return 'version/';
|
return 'version/';
|
||||||
case ApiPaths.sso_providers:
|
case ApiPaths.sso_providers:
|
||||||
return 'auth/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:
|
case ApiPaths.build_order_list:
|
||||||
return 'build/';
|
return 'build/';
|
||||||
case ApiPaths.build_order_attachment_list:
|
case ApiPaths.build_order_attachment_list:
|
||||||
|
Reference in New Issue
Block a user