2
0
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:
Matthias Mair
2026-05-22 01:44:24 +02:00
committed by GitHub
parent f27b9b5443
commit 9908870a81
12 changed files with 72 additions and 17 deletions
+1
View File
@@ -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
+2 -2
View File
@@ -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
+21
View File
@@ -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
+32 -3
View File
@@ -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 = [
# Legacy endpoints (to avoid breaking existing API clients)
# 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('roles/', RoleDetails.as_view(), name='api-user-roles'),
path('token/', ensure_csrf_cookie(GetAuthToken.as_view()), name='api-token'), 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([
+5 -4
View File
@@ -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`
+1 -1
View File
@@ -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');
} }
+1 -1
View File
@@ -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: {} },
+1 -1
View File
@@ -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'],