Feature: openapi spec, full api browser (#8948)

This commit is contained in:
shamoon
2025-02-10 08:43:07 -08:00
committed by GitHub
parent c316ae369b
commit 1dc80f04cb
19 changed files with 1048 additions and 255 deletions

View File

@@ -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 = (

View File

@@ -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:

View File

@@ -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,
],
),

View File

@@ -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
"""