mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-22 01:06:50 +00:00
realign user API endpoints (#11963)
* realign user API endpoints to make it clearer which one are only applicable to the current user * fix name * bump api * fix test * fix reference * fix test exception * update ref * reduce breakage * re-add legacy urls till next `breaking`
This commit is contained in:
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
- [#9604](https://github.com/inventree/InvenTree/pull/9604) - refactors user API endpoint to be less ambiguous
|
||||||
- [#11893](https://github.com/inventree/InvenTree/pull/11893) bumps Node environment to version 24 LTS - this is only relevant if you build the frontend assets yourself
|
- [#11893](https://github.com/inventree/InvenTree/pull/11893) bumps Node environment to version 24 LTS - this is only relevant if you build the frontend assets yourself
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Each user is assigned an authentication token which can be used to access the AP
|
|||||||
|
|
||||||
If a user does not know their access token, it can be requested via the API interface itself, using a basic authentication request.
|
If a user does not know their access token, it can be requested via the API interface itself, using a basic authentication request.
|
||||||
|
|
||||||
To obtain a valid token, perform a GET request to `/api/user/token/`. No data are required, but a valid username / password combination must be supplied in the authentication headers.
|
To obtain a valid token, perform a GET request to `/api/user/me/token/`. No data are required, but a valid username / password combination must be supplied in the authentication headers.
|
||||||
|
|
||||||
!!! info "Credentials"
|
!!! info "Credentials"
|
||||||
Ensure that a valid username:password combination are supplied as basic authorization headers.
|
Ensure that a valid username:password combination are supplied as basic authorization headers.
|
||||||
@@ -146,7 +146,7 @@ r:delete:stock
|
|||||||
Users can only perform REST API actions which align with their assigned [role permissions](../settings/permissions.md#roles).
|
Users can only perform REST API actions which align with their assigned [role permissions](../settings/permissions.md#roles).
|
||||||
Once a user has *authenticated* via the API, a list of the available roles can be retrieved from:
|
Once a user has *authenticated* via the API, a list of the available roles can be retrieved from:
|
||||||
|
|
||||||
`/api/user/roles/`
|
`/api/user/me/roles/`
|
||||||
|
|
||||||
For example, when accessing the API from a *superuser* account:
|
For example, when accessing the API from a *superuser* account:
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 489
|
INVENTREE_API_VERSION = 490
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v490 -> 2026-05-19 : https://github.com/inventree/InvenTree/pull/11963
|
||||||
|
- moves user-self-filtered endpoints to /user/me/ to make their security boundaries clearer
|
||||||
|
|
||||||
v489 -> 2026-05-18 : https://github.com/inventree/InvenTree/pull/11962
|
v489 -> 2026-05-18 : https://github.com/inventree/InvenTree/pull/11962
|
||||||
- Removes the "remote_image" field from the Part API endpoint
|
- Removes the "remote_image" field from the Part API endpoint
|
||||||
- Removes the "remote_image" field from the Company API endpoint
|
- Removes the "remote_image" field from the Company API endpoint
|
||||||
|
|||||||
@@ -341,3 +341,24 @@ def schema_for_view_output_options(view_class):
|
|||||||
view_class
|
view_class
|
||||||
)
|
)
|
||||||
return extended_view
|
return extended_view
|
||||||
|
|
||||||
|
|
||||||
|
def exclude_from_schema(klass: type, alternative_path: str) -> type:
|
||||||
|
"""Decorator to exclude a view from the OpenAPI schema.
|
||||||
|
|
||||||
|
This is used to hide legacy endpoints from the schema, while still retaining them for backwards compatibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class LegacyView(klass):
|
||||||
|
"""Dummy doc."""
|
||||||
|
|
||||||
|
LegacyView.__name__ = klass.__name__ + ' - Legacy'
|
||||||
|
LegacyView.__doc__ = f'This is a legacy endpoint, retained for backwards compatibility. Consider migrating to the new endpoint under {alternative_path}.'
|
||||||
|
|
||||||
|
# Exclude all default operations from the schema
|
||||||
|
for operation in ['get', 'post', 'put', 'patch', 'delete']:
|
||||||
|
if hasattr(klass, operation):
|
||||||
|
LegacyView = extend_schema_view(**{operation: extend_schema(exclude=True)})(
|
||||||
|
LegacyView
|
||||||
|
)
|
||||||
|
return LegacyView
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from InvenTree.mixins import (
|
|||||||
SerializerContextMixin,
|
SerializerContextMixin,
|
||||||
UpdateAPI,
|
UpdateAPI,
|
||||||
)
|
)
|
||||||
|
from InvenTree.schema import exclude_from_schema
|
||||||
from InvenTree.settings import FRONTEND_URL_BASE
|
from InvenTree.settings import FRONTEND_URL_BASE
|
||||||
from users.models import ApiToken, Owner, RuleSet, UserProfile
|
from users.models import ApiToken, Owner, RuleSet, UserProfile
|
||||||
from users.serializers import (
|
from users.serializers import (
|
||||||
@@ -501,8 +502,38 @@ class UserProfileDetail(RetrieveUpdateAPI):
|
|||||||
|
|
||||||
|
|
||||||
user_urls = [
|
user_urls = [
|
||||||
path('roles/', RoleDetails.as_view(), name='api-user-roles'),
|
# Legacy endpoints (to avoid breaking existing API clients)
|
||||||
path('token/', ensure_csrf_cookie(GetAuthToken.as_view()), name='api-token'),
|
# TODO @matmair - remove these legacy endpoints in the next breaking release
|
||||||
|
path(
|
||||||
|
'roles/',
|
||||||
|
exclude_from_schema(RoleDetails, '/api/user/me/roles/').as_view(),
|
||||||
|
name='api-user-roles_legacy',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'token/',
|
||||||
|
ensure_csrf_cookie(
|
||||||
|
exclude_from_schema(GetAuthToken, '/api/user/me/token/').as_view()
|
||||||
|
),
|
||||||
|
name='api-token_legacy',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'profile/',
|
||||||
|
exclude_from_schema(UserProfileDetail, '/api/user/me/profile/').as_view(),
|
||||||
|
name='api-user-profile_legacy',
|
||||||
|
),
|
||||||
|
# Individual user endpoints
|
||||||
|
path(
|
||||||
|
'me/',
|
||||||
|
include([
|
||||||
|
path('profile/', UserProfileDetail.as_view(), name='api-user-profile'),
|
||||||
|
path('roles/', RoleDetails.as_view(), name='api-user-roles'),
|
||||||
|
path(
|
||||||
|
'token/', ensure_csrf_cookie(GetAuthToken.as_view()), name='api-token'
|
||||||
|
),
|
||||||
|
path('', MeUserDetail.as_view(), name='api-user-me'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
# User related endpoints
|
||||||
path(
|
path(
|
||||||
'tokens/',
|
'tokens/',
|
||||||
include([
|
include([
|
||||||
@@ -510,8 +541,6 @@ user_urls = [
|
|||||||
path('', TokenListView.as_view(), name='api-token-list'),
|
path('', TokenListView.as_view(), name='api-token-list'),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
path('me/', MeUserDetail.as_view(), name='api-user-me'),
|
|
||||||
path('profile/', UserProfileDetail.as_view(), name='api-user-profile'),
|
|
||||||
path(
|
path(
|
||||||
'owner/',
|
'owner/',
|
||||||
include([
|
include([
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ export enum ApiEndpoints {
|
|||||||
// User API endpoints
|
// User API endpoints
|
||||||
user_list = 'user/',
|
user_list = 'user/',
|
||||||
user_set_password = 'user/:id/set-password/',
|
user_set_password = 'user/:id/set-password/',
|
||||||
user_me = 'user/me/',
|
|
||||||
user_profile = 'user/profile/',
|
|
||||||
user_roles = 'user/roles/',
|
|
||||||
user_token = 'user/token/',
|
|
||||||
user_tokens = 'user/tokens/',
|
user_tokens = 'user/tokens/',
|
||||||
user_simple_login = 'email/generate/',
|
user_simple_login = 'email/generate/',
|
||||||
|
// Individual user endpoints
|
||||||
|
user_me_profile = 'user/me/profile/',
|
||||||
|
user_me_roles = 'user/me/roles/',
|
||||||
|
user_me_token = 'user/me/token/',
|
||||||
|
user_me = 'user/me/',
|
||||||
|
|
||||||
// User auth endpoints
|
// User auth endpoints
|
||||||
auth_base = '/auth/',
|
auth_base = '/auth/',
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function AccountDetailPanel() {
|
|||||||
|
|
||||||
const editProfile = useEditApiFormModal({
|
const editProfile = useEditApiFormModal({
|
||||||
title: t`Edit Profile Information`,
|
title: t`Edit Profile Information`,
|
||||||
url: ApiEndpoints.user_profile,
|
url: ApiEndpoints.user_me_profile,
|
||||||
onFormSuccess: fetchUserState,
|
onFormSuccess: fetchUserState,
|
||||||
fields: profileFields,
|
fields: profileFields,
|
||||||
successMessage: t`Profile details updated`
|
successMessage: t`Profile details updated`
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ pushes changes in user profile to backend
|
|||||||
export function patchUser(key: 'language' | 'theme' | 'widgets', val: any) {
|
export function patchUser(key: 'language' | 'theme' | 'widgets', val: any) {
|
||||||
const uid = useUserState.getState().userId();
|
const uid = useUserState.getState().userId();
|
||||||
if (uid) {
|
if (uid) {
|
||||||
api.patch(apiUrl(ApiEndpoints.user_profile), { [key]: val });
|
api.patch(apiUrl(ApiEndpoints.user_me_profile), { [key]: val });
|
||||||
} else {
|
} else {
|
||||||
console.log('user not logged in, not patching');
|
console.log('user not logged in, not patching');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
|
|||||||
|
|
||||||
// Fetch role data
|
// Fetch role data
|
||||||
await api
|
await api
|
||||||
.get(apiUrl(ApiEndpoints.user_roles))
|
.get(apiUrl(ApiEndpoints.user_me_roles))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
const user: UserProps = get().user as UserProps;
|
const user: UserProps = get().user as UserProps;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function ApiTokenTable({
|
|||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
const generateToken = useCreateApiFormModal({
|
const generateToken = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.user_token,
|
url: ApiEndpoints.user_me_token,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
title: t`Generate Token`,
|
title: t`Generate Token`,
|
||||||
fields: { name: {} },
|
fields: { name: {} },
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const test = baseTest.extend({
|
|||||||
!msg.text().includes('/this/does/not/exist.js') &&
|
!msg.text().includes('/this/does/not/exist.js') &&
|
||||||
!url.includes('/this/does/not/exist.js') &&
|
!url.includes('/this/does/not/exist.js') &&
|
||||||
!url.includes('/api/user/me/') &&
|
!url.includes('/api/user/me/') &&
|
||||||
!url.includes('/api/user/token/') &&
|
!url.includes('/api/user/me/token/') &&
|
||||||
!url.includes('/api/auth/v1/auth/login') &&
|
!url.includes('/api/auth/v1/auth/login') &&
|
||||||
!url.includes('/api/auth/v1/auth/session') &&
|
!url.includes('/api/auth/v1/auth/session') &&
|
||||||
!url.includes('/api/auth/v1/account/authenticators/totp') &&
|
!url.includes('/api/auth/v1/account/authenticators/totp') &&
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ test('Dashboard - Preserve widget sizes', async ({ browser }) => {
|
|||||||
password: user.testcred
|
password: user.testcred
|
||||||
});
|
});
|
||||||
|
|
||||||
(await api).patch('user/profile/', {
|
(await api).patch('user/me/profile/', {
|
||||||
data: {
|
data: {
|
||||||
widgets: {
|
widgets: {
|
||||||
widgets: ['ovr-so'],
|
widgets: ['ovr-so'],
|
||||||
|
|||||||
Reference in New Issue
Block a user