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
|
||||
|
||||
- [#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
|
||||
|
||||
### 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.
|
||||
|
||||
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"
|
||||
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).
|
||||
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:
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# 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."""
|
||||
|
||||
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
|
||||
- Removes the "remote_image" field from the Part 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
|
||||
)
|
||||
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,
|
||||
UpdateAPI,
|
||||
)
|
||||
from InvenTree.schema import exclude_from_schema
|
||||
from InvenTree.settings import FRONTEND_URL_BASE
|
||||
from users.models import ApiToken, Owner, RuleSet, UserProfile
|
||||
from users.serializers import (
|
||||
@@ -501,8 +502,38 @@ class UserProfileDetail(RetrieveUpdateAPI):
|
||||
|
||||
|
||||
user_urls = [
|
||||
path('roles/', RoleDetails.as_view(), name='api-user-roles'),
|
||||
path('token/', ensure_csrf_cookie(GetAuthToken.as_view()), name='api-token'),
|
||||
# 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(
|
||||
'token/', ensure_csrf_cookie(GetAuthToken.as_view()), name='api-token'
|
||||
),
|
||||
path('', MeUserDetail.as_view(), name='api-user-me'),
|
||||
]),
|
||||
),
|
||||
# User related endpoints
|
||||
path(
|
||||
'tokens/',
|
||||
include([
|
||||
@@ -510,8 +541,6 @@ user_urls = [
|
||||
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(
|
||||
'owner/',
|
||||
include([
|
||||
|
||||
@@ -12,12 +12,13 @@ export enum ApiEndpoints {
|
||||
// User API endpoints
|
||||
user_list = 'user/',
|
||||
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_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
|
||||
auth_base = '/auth/',
|
||||
|
||||
@@ -53,7 +53,7 @@ export function AccountDetailPanel() {
|
||||
|
||||
const editProfile = useEditApiFormModal({
|
||||
title: t`Edit Profile Information`,
|
||||
url: ApiEndpoints.user_profile,
|
||||
url: ApiEndpoints.user_me_profile,
|
||||
onFormSuccess: fetchUserState,
|
||||
fields: profileFields,
|
||||
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) {
|
||||
const uid = useUserState.getState().userId();
|
||||
if (uid) {
|
||||
api.patch(apiUrl(ApiEndpoints.user_profile), { [key]: val });
|
||||
api.patch(apiUrl(ApiEndpoints.user_me_profile), { [key]: val });
|
||||
} else {
|
||||
console.log('user not logged in, not patching');
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
|
||||
|
||||
// Fetch role data
|
||||
await api
|
||||
.get(apiUrl(ApiEndpoints.user_roles))
|
||||
.get(apiUrl(ApiEndpoints.user_me_roles))
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
const user: UserProps = get().user as UserProps;
|
||||
|
||||
@@ -26,7 +26,7 @@ export function ApiTokenTable({
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
const generateToken = useCreateApiFormModal({
|
||||
url: ApiEndpoints.user_token,
|
||||
url: ApiEndpoints.user_me_token,
|
||||
method: 'GET',
|
||||
title: t`Generate Token`,
|
||||
fields: { name: {} },
|
||||
|
||||
@@ -70,7 +70,7 @@ export const test = baseTest.extend({
|
||||
!msg.text().includes('/this/does/not/exist.js') &&
|
||||
!url.includes('/this/does/not/exist.js') &&
|
||||
!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/session') &&
|
||||
!url.includes('/api/auth/v1/account/authenticators/totp') &&
|
||||
|
||||
@@ -142,7 +142,7 @@ test('Dashboard - Preserve widget sizes', async ({ browser }) => {
|
||||
password: user.testcred
|
||||
});
|
||||
|
||||
(await api).patch('user/profile/', {
|
||||
(await api).patch('user/me/profile/', {
|
||||
data: {
|
||||
widgets: {
|
||||
widgets: ['ovr-so'],
|
||||
|
||||
Reference in New Issue
Block a user