mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-10-04 01:52:42 -05:00
Feature: openapi spec, full api browser (#8948)
This commit is contained in:
@@ -11,22 +11,11 @@ from rest_framework import serializers
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
|
||||
from paperless.models import ApplicationConfiguration
|
||||
from paperless_mail.serialisers import ObfuscatedPasswordField
|
||||
|
||||
logger = logging.getLogger("paperless.settings")
|
||||
|
||||
|
||||
class ObfuscatedUserPasswordField(serializers.Field):
|
||||
"""
|
||||
Sends *** string instead of password in the clear
|
||||
"""
|
||||
|
||||
def to_representation(self, value):
|
||||
return "**********" if len(value) > 0 else ""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
|
||||
|
||||
class PaperlessAuthTokenSerializer(AuthTokenSerializer):
|
||||
code = serializers.CharField(
|
||||
label="MFA Code",
|
||||
@@ -58,7 +47,7 @@ class PaperlessAuthTokenSerializer(AuthTokenSerializer):
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
password = ObfuscatedUserPasswordField(required=False)
|
||||
password = ObfuscatedPasswordField(required=False)
|
||||
user_permissions = serializers.SlugRelatedField(
|
||||
many=True,
|
||||
queryset=Permission.objects.exclude(content_type__app_label="admin"),
|
||||
@@ -68,7 +57,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
inherited_permissions = serializers.SerializerMethodField()
|
||||
is_mfa_enabled = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_mfa_enabled(self, user: User):
|
||||
def get_is_mfa_enabled(self, user: User) -> bool:
|
||||
mfa_adapter = get_mfa_adapter()
|
||||
return mfa_adapter.is_mfa_enabled(user)
|
||||
|
||||
@@ -91,7 +80,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
"is_mfa_enabled",
|
||||
)
|
||||
|
||||
def get_inherited_permissions(self, obj):
|
||||
def get_inherited_permissions(self, obj) -> list[str]:
|
||||
return obj.get_group_permissions()
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@@ -157,13 +146,13 @@ class SocialAccountSerializer(serializers.ModelSerializer):
|
||||
"name",
|
||||
)
|
||||
|
||||
def get_name(self, obj):
|
||||
def get_name(self, obj) -> str:
|
||||
return obj.get_provider_account().to_str()
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
email = serializers.EmailField(allow_blank=True, required=False)
|
||||
password = ObfuscatedUserPasswordField(required=False, allow_null=False)
|
||||
password = ObfuscatedPasswordField(required=False, allow_null=False)
|
||||
auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key")
|
||||
social_accounts = SocialAccountSerializer(
|
||||
many=True,
|
||||
@@ -171,11 +160,15 @@ class ProfileSerializer(serializers.ModelSerializer):
|
||||
source="socialaccount_set",
|
||||
)
|
||||
is_mfa_enabled = serializers.SerializerMethodField()
|
||||
has_usable_password = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_mfa_enabled(self, user: User):
|
||||
def get_is_mfa_enabled(self, user: User) -> bool:
|
||||
mfa_adapter = get_mfa_adapter()
|
||||
return mfa_adapter.is_mfa_enabled(user)
|
||||
|
||||
def get_has_usable_password(self, user: User) -> bool:
|
||||
return user.has_usable_password()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
|
@@ -328,6 +328,8 @@ INSTALLED_APPS = [
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"allauth.mfa",
|
||||
"drf_spectacular",
|
||||
"drf_spectacular_sidecar",
|
||||
*env_apps,
|
||||
]
|
||||
|
||||
@@ -345,6 +347,25 @@ REST_FRAMEWORK = {
|
||||
# Make sure these are ordered and that the most recent version appears
|
||||
# last. See api.md#api-versioning when adding new versions.
|
||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6", "7"],
|
||||
# DRF Spectacular default schema
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
# DRF Spectacular settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Paperless-ngx REST API",
|
||||
"DESCRIPTION": "OpenAPI Spec for Paperless-ngx",
|
||||
"VERSION": "6.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SWAGGER_UI_DIST": "SIDECAR",
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"EXTERNAL_DOCS": {
|
||||
"description": "Paperless-ngx API Documentation",
|
||||
"url": "https://docs.paperless-ngx.com/api/",
|
||||
},
|
||||
"ENUM_NAME_OVERRIDES": {
|
||||
"MatchingAlgorithm": "documents.models.MatchingModel.MATCHING_ALGORITHMS",
|
||||
},
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
|
@@ -14,6 +14,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import RedirectView
|
||||
from django.views.static import serve
|
||||
from drf_spectacular.views import SpectacularAPIView
|
||||
from drf_spectacular.views import SpectacularSwaggerView
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from documents.views import BulkDownloadView
|
||||
@@ -203,6 +205,27 @@ urlpatterns = [
|
||||
OauthCallbackView.as_view(),
|
||||
name="oauth_callback",
|
||||
),
|
||||
re_path(
|
||||
"^schema/",
|
||||
include(
|
||||
[
|
||||
re_path(
|
||||
"^$",
|
||||
SpectacularAPIView.as_view(),
|
||||
name="schema",
|
||||
),
|
||||
re_path(
|
||||
"^view/",
|
||||
SpectacularSwaggerView.as_view(),
|
||||
name="swagger-ui",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
re_path(
|
||||
"^$", # Redirect to the API swagger view
|
||||
RedirectView.as_view(url="schema/view/"),
|
||||
),
|
||||
*api_router.urls,
|
||||
],
|
||||
),
|
||||
|
@@ -18,6 +18,9 @@ from django.http import HttpResponseForbidden
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.views.generic import View
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.utils import extend_schema_view
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.authtoken.views import ObtainAuthToken
|
||||
from rest_framework.decorators import action
|
||||
@@ -27,7 +30,6 @@ from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.permissions import DjangoModelPermissions
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from documents.permissions import PaperlessObjectPermissions
|
||||
@@ -197,6 +199,34 @@ class ProfileView(GenericAPIView):
|
||||
return Response(serializer.to_representation(user))
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
responses={
|
||||
(200, "application/json"): OpenApiTypes.OBJECT,
|
||||
},
|
||||
),
|
||||
post=extend_schema(
|
||||
request={
|
||||
"application/json": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {"type": "string"},
|
||||
"code": {"type": "string"},
|
||||
},
|
||||
"required": ["secret", "code"],
|
||||
},
|
||||
},
|
||||
responses={
|
||||
(200, "application/json"): OpenApiTypes.OBJECT,
|
||||
},
|
||||
),
|
||||
delete=extend_schema(
|
||||
responses={
|
||||
(200, "application/json"): OpenApiTypes.BOOL,
|
||||
404: OpenApiTypes.STR,
|
||||
},
|
||||
),
|
||||
)
|
||||
class TOTPView(GenericAPIView):
|
||||
"""
|
||||
TOTP views
|
||||
@@ -267,6 +297,16 @@ class TOTPView(GenericAPIView):
|
||||
return HttpResponseNotFound("TOTP not found")
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
request={
|
||||
"application/json": None,
|
||||
},
|
||||
responses={
|
||||
(200, "application/json"): OpenApiTypes.STR,
|
||||
},
|
||||
),
|
||||
)
|
||||
class GenerateAuthTokenView(GenericAPIView):
|
||||
"""
|
||||
Generates (or re-generates) an auth token, requires a logged in user
|
||||
@@ -287,6 +327,15 @@ class GenerateAuthTokenView(GenericAPIView):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
description="Get the application configuration",
|
||||
external_docs={
|
||||
"description": "Application Configuration",
|
||||
"url": "https://docs.paperless-ngx.com/configuration/",
|
||||
},
|
||||
),
|
||||
)
|
||||
class ApplicationConfigurationViewSet(ModelViewSet):
|
||||
model = ApplicationConfiguration
|
||||
|
||||
@@ -296,6 +345,23 @@ class ApplicationConfigurationViewSet(ModelViewSet):
|
||||
permission_classes = (IsAuthenticated, DjangoModelPermissions)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
request={
|
||||
"application/json": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer"},
|
||||
},
|
||||
"required": ["id"],
|
||||
},
|
||||
},
|
||||
responses={
|
||||
(200, "application/json"): OpenApiTypes.INT,
|
||||
400: OpenApiTypes.STR,
|
||||
},
|
||||
),
|
||||
)
|
||||
class DisconnectSocialAccountView(GenericAPIView):
|
||||
"""
|
||||
Disconnects a social account provider from the user account
|
||||
@@ -315,7 +381,14 @@ class DisconnectSocialAccountView(GenericAPIView):
|
||||
return HttpResponseBadRequest("Social account not found")
|
||||
|
||||
|
||||
class SocialAccountProvidersView(APIView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
responses={
|
||||
(200, "application/json"): OpenApiTypes.OBJECT,
|
||||
},
|
||||
),
|
||||
)
|
||||
class SocialAccountProvidersView(GenericAPIView):
|
||||
"""
|
||||
List of social account providers
|
||||
"""
|
||||
|
Reference in New Issue
Block a user