perf: support generate webui_schema.json such as form, serializer

This commit is contained in:
Bai
2026-01-19 16:08:07 +08:00
parent 669823155f
commit 4571887986
8 changed files with 3058 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
from django.core.cache import cache
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
from rest_framework.exceptions import NotFound
from rest_framework.generics import CreateAPIView, RetrieveAPIView
from rest_framework.permissions import AllowAny
@@ -104,6 +105,7 @@ class FaceCallbackApi(AuthMixin, CreateAPIView):
class FaceContextApi(AuthMixin, RetrieveAPIView, CreateAPIView):
permission_classes = (AllowAny,)
face_token_session_key = FACE_SESSION_KEY
serializer_class = Serializer
@staticmethod
def get_face_cache_key(token):

View File

@@ -6,6 +6,7 @@ from django.contrib.auth import logout
from django.contrib.auth.models import AnonymousUser
from rest_framework import generics
from rest_framework import status
from rest_framework.serializers import Serializer
from rest_framework.response import Response
from common.sessions.cache import user_session_manager
@@ -52,6 +53,7 @@ class UserSessionManager:
class UserSessionApi(generics.RetrieveDestroyAPIView):
permission_classes = ()
serializer_class = Serializer
def retrieve(self, request, *args, **kwargs):
if isinstance(request.user, AnonymousUser):

View File

@@ -81,3 +81,13 @@ def get_user_login_form_cls(*, captcha=False):
bases.append(CaptchaMixin)
bases.append(UserLoginForm)
return type('UserLoginForm', tuple(bases), {})
def get_comprehensive_user_login_form_cls():
bases = [
ChallengeMixin,
UserCheckOtpCodeForm,
CaptchaMixin,
UserLoginForm
]
return type('UserLoginForm', tuple(bases), {})

View File

@@ -30,7 +30,7 @@ from users.utils import (
)
from .. import mixins, errors
from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY, USER_LOGIN_GUARD_VIEW_REDIRECT_FIELD
from ..forms import get_user_login_form_cls
from ..forms import get_user_login_form_cls, get_comprehensive_user_login_form_cls
from ..utils import get_auth_methods
__all__ = [
@@ -253,6 +253,9 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
return get_user_login_form_cls(captcha=True)
else:
return get_user_login_form_cls()
def get_comprehensive_form_class(self):
return get_comprehensive_user_login_form_cls()
def clear_rsa_key(self):
self.request.session[RSA_PRIVATE_KEY] = None

View File

@@ -3,6 +3,7 @@ import os
from django.conf import settings
from django.utils._os import safe_join
from rest_framework.serializers import Serializer
from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
@@ -14,6 +15,7 @@ class ComponentI18nApi(RetrieveAPIView):
base_path = 'locale'
permission_classes = [AllowAny]
lang_data = {}
serializer_class = Serializer
def get_component_translations(self, name):
if not settings.DEBUG and name in self.lang_data:

View File

@@ -0,0 +1,537 @@
import json
import os
import sys
from turtle import up
import django
from django.urls import get_resolver
from django.urls.resolvers import URLPattern, URLResolver
# 获取项目根目录jumpserver 目录)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
APP_DIR = os.path.join(BASE_DIR, 'apps')
# 不改变工作目录,直接加入 sys.path
sys.path.insert(0, APP_DIR)
sys.path.insert(0, BASE_DIR)
# 设置 Django 环境
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
django.setup()
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
OUTPUT_FILE_DIR = os.path.join(CURRENT_DIR, 'output')
os.makedirs(OUTPUT_FILE_DIR, exist_ok=True)
from rest_framework.views import APIView
from rest_framework.permissions import OperandHolder, AND, OR, NOT
from django.views.generic.base import View
from django.views.generic.edit import FormView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.handlers.asgi import ASGIRequest
from rest_framework.permissions import AllowAny, IsAuthenticated
from rbac.permissions import RBACPermission
from common.utils import lazyproperty
# 尝试使用模拟请求
scope = {
'type': 'http',
'method': 'GET',
'path': '/',
'query_string': b'',
'headers': [],
}
async def receive():
return {'type': 'http.request', 'body': b''}
request = ASGIRequest(scope, receive)
def log(message):
print(message)
class FieldsGenerator:
def write_fields_schema(self):
return {}
def required_fields(self):
return []
class FormFieldsGenerator(FieldsGenerator):
def __init__(self, raw_class, view):
self.raw_class = raw_class
self.view = view
@lazyproperty
def fields(self):
return self.raw_class().fields
def write_fields_schema(self):
schema = self.get_fields_schema(self.fields)
return schema
def required_fields(self):
fields = [name for name, field in self.fields.items() if field.required]
return fields
def get_fields_schema(self, fields):
schemas = {}
for name, field in fields.items():
schema = {
'type': self.get_field_type(field),
}
description = getattr(field, 'help_text', '')
if description:
schema['description'] = str(description)
schemas[name] = schema
return schemas
def get_field_type(self, field):
"""将 Django Form Field 类型映射到 JSON Schema 类型"""
from django import forms
type_mapping = {
forms.CharField: 'string',
forms.EmailField: 'string',
forms.URLField: 'string',
forms.SlugField: 'string',
forms.UUIDField: 'string',
forms.RegexField: 'string',
forms.FileField: 'string',
forms.ImageField: 'string',
forms.FilePathField: 'string',
forms.GenericIPAddressField: 'string',
forms.IntegerField: 'integer',
forms.FloatField: 'number',
forms.DecimalField: 'number',
forms.BooleanField: 'boolean',
forms.NullBooleanField: 'boolean',
forms.DateField: 'string',
forms.TimeField: 'string',
forms.DateTimeField: 'string',
forms.DurationField: 'string',
forms.MultipleChoiceField: 'array',
forms.TypedMultipleChoiceField: 'array',
forms.ModelMultipleChoiceField: 'array',
forms.ChoiceField: 'string',
forms.TypedChoiceField: 'string',
forms.ModelChoiceField: 'string',
forms.JSONField: 'object',
}
for field_type, json_type in type_mapping.items():
if isinstance(field, field_type):
return json_type
return 'string'
class SerializerFieldsGenerator(FieldsGenerator):
def __init__(self, raw_class, view):
self.raw_class = raw_class
self.view = view
@lazyproperty
def fields(self):
fields = {}
try:
fields = self.raw_class().fields
except Exception as e:
if hasattr(self.raw_class, '_declared_fields'):
fields = self.raw_class._declared_fields
return fields
@lazyproperty
def write_fields(self):
fields = {}
for name, field in self.fields.items():
if field.read_only:
continue
fields[name] = field
return fields
def get_fields_schema(self, fields):
schemas = {}
if not hasattr(fields, 'items'):
return {}
for name, field in fields.items():
schema = self.get_field_schema(field)
if hasattr(field, 'child'):
_fields = field.child
_fields_schema = self.get_fields_schema(_fields)
schema['properties'] = _fields_schema
schemas[name] = schema
return schemas
def get_field_schema(self, field):
schema = {
'type': self.get_field_type(field),
}
if description := getattr(field, 'help_text', ''):
schema['description'] = str(description)
extra_schema = self.get_field_extra_schema(field)
schema.update(extra_schema)
return schema
def field_is_nested(self, field):
from rest_framework import serializers
nested_field_types = (
serializers.Serializer,
serializers.ModelSerializer,
)
return isinstance(field, nested_field_types)
def get_field_extra_schema(self, field):
"""获取字段的正则表达式模式"""
patterns = {}
# 检查 validators 中是否有正则验证器
if hasattr(field, 'validators'):
for validator in field.validators:
if hasattr(validator, 'regex'):
patterns['pattern'] = str(validator.regex.pattern)
break
# 针对特定字段类型添加模式
from rest_framework import serializers
if isinstance(field, serializers.EmailField):
patterns['format'] = 'email'
elif isinstance(field, serializers.URLField):
patterns['format'] = 'uri'
elif isinstance(field, (serializers.DateTimeField, serializers.DateField, serializers.TimeField)):
patterns['format'] = 'date-time'
# 添加长度限制
if hasattr(field, 'max_length') and field.max_length:
patterns['maxLength'] = field.max_length
if hasattr(field, 'min_length') and field.min_length:
patterns['minLength'] = field.min_length
# 添加数值范围
if hasattr(field, 'max_value') and field.max_value is not None:
patterns['maximum'] = field.max_value
if hasattr(field, 'min_value') and field.min_value is not None:
patterns['minimum'] = field.min_value
if choices := self.get_field_choices(field):
patterns['enum'] = choices
return patterns
def get_field_choices(self, field):
from rest_framework import serializers
choices = []
field_need_query_db = isinstance(field, (
serializers.PrimaryKeyRelatedField, # 会查询数据库
serializers.StringRelatedField, # 会查询数据库
serializers.SlugRelatedField, # 会查询数据库
serializers.HyperlinkedRelatedField, # 会查询数据库
serializers.HyperlinkedIdentityField,# 会查询数据库
serializers.RelatedField, # 基类,会查询数据库
serializers.ManyRelatedField, # 会查询数据库
serializers.ListSerializer # 可能会查询数据库
))
if field_need_query_db:
return choices # 不返回需要查询数据库的字段选项
choices = getattr(field, 'choices', [])
if not choices:
return choices
if isinstance(choices, dict):
# choices 可能是字典、列表或元组
choices = list(choices.keys())
elif isinstance(choices, (list, tuple)):
# choices 可能是 [(value, label), ...] 或 [value, ...]
for choice in choices:
if isinstance(choice, (list, tuple)) and len(choice) == 2:
choices.append(choice[0])
else:
choices.append(choice)
return choices
def get_field_type(self, field):
"""将 Python 字段类型映射到 JSON Schema 类型"""
from rest_framework import serializers
if self.field_is_nested(field):
return 'object'
type_mapping = {
serializers.CharField: 'string',
serializers.EmailField: 'string',
serializers.URLField: 'string',
serializers.UUIDField: 'string',
serializers.SlugField: 'string',
serializers.ChoiceField: 'string',
serializers.IntegerField: 'integer',
serializers.FloatField: 'number',
serializers.DecimalField: 'number',
serializers.BooleanField: 'boolean',
serializers.DateTimeField: 'string',
serializers.DateField: 'string',
serializers.TimeField: 'string',
serializers.ListField: 'array',
serializers.DictField: 'object',
serializers.JSONField: 'object',
}
for field_type, json_type in type_mapping.items():
if isinstance(field, field_type):
return json_type
return 'string' # 默认类型
def write_fields_schema(self):
fields = self.write_fields
if not fields:
return {}
schema = self.get_fields_schema(fields)
return schema
def required_fields(self):
required = []
for name, field in self.write_fields.items():
if field.required:
required.append(name)
return required
class CustomView:
def __init__(self, view_func):
self.view_func = view_func
@property
def view_class(self):
cls = getattr(self.view_func, 'view_class', None)
if not cls:
cls = getattr(self.view_func, 'cls', None)
return cls
@property
def view_path(self):
if self.view_class:
v = self.view_class
else:
v = self.view_func
return f'{v.__module__}.{v.__name__}'
@property
def view_type(self):
if self.view_class:
return 'class'
else:
return 'function'
@lazyproperty
def fields_generator(self):
generator = None
if self.view_class:
if issubclass(self.view_class, FormView):
generator = self.get_form_fields_generator()
if issubclass(self.view_class, APIView):
generator= self.get_serializer_fields_generator()
if not generator:
generator = FieldsGenerator()
return generator
@property
def write_fields_schema(self):
return self.fields_generator.write_fields_schema()
@property
def required_fields(self):
return self.fields_generator.required_fields()
def get_form_fields_generator(self):
if hasattr(self.view_class, 'get_comprehensive_form_class'):
view_instance = self.view_class(request=request)
form_class = view_instance.get_comprehensive_form_class()
else:
form_class = getattr(self.view_class, 'form_class', None)
if not form_class:
if hasattr(self.view_class, 'get_form_class'):
# TODO: 实例化 view 类需要传入 request 参数
view_instance = self.view_class(request=request)
form_class = view_instance.get_form_class()
if form_class:
return FormFieldsGenerator(raw_class=form_class, view=self)
def get_serializer_fields_generator(self):
serializer_class = getattr(self.view_class, 'serializer_class', None)
if not serializer_class:
if hasattr(self.view_class, 'get_serializer_class'):
# TODO: 实例化 view 类需要传入 request 参数
view_instance = self.view_class(request=request)
serializer_class = view_instance.get_serializer_class()
if serializer_class:
return SerializerFieldsGenerator(raw_class=serializer_class, view=self)
@property
def query_fields_schema(self):
return {}
@lazyproperty
def requires_auth(self):
if self.view_class:
return self.check_view_class_requires_auth()
else:
return self.check_view_func_requires_auth()
def check_view_class_requires_auth(self):
if issubclass(self.view_class, LoginRequiredMixin):
return True
permission_classes = getattr(self.view_class, 'permission_classes', [])
if not permission_classes:
return False
return self.check_permission_classes_requires_auth(permission_classes)
def check_permission_classes_requires_auth(self, permission_classes, operator=AND):
if operator == AND:
for pc in permission_classes:
if self.check_permission_class_requires_auth(pc):
return True
return False
elif operator == OR:
for pc in permission_classes:
if not self.check_permission_class_requires_auth(pc):
return False
return True
elif operator == NOT:
raise ValueError('NOT operator is not supported in permission_classes')
else:
return False
def check_permission_class_requires_auth(self, permission_class):
if isinstance(permission_class, OperandHolder):
operator = permission_class.operator_class
op1_class = permission_class.op1_class
op2_class = permission_class.op2_class
permission_classes = [op1_class, op2_class]
return self.check_permission_classes_requires_auth(permission_classes, operator)
else:
if issubclass(permission_class, (IsAuthenticated, RBACPermission)):
return True
if issubclass(permission_class, (AllowAny, )):
return False
if hasattr(permission_class, '__name__'):
if 'Authenticated' in permission_class.__name__:
return True
if permission_class.__name__.startswith('UserConfirmation'):
return True
return False
def check_view_func_requires_auth(self):
if hasattr(self.view_func, '__wrapped__'):
if hasattr(self.view_func, '__name__'):
if 'login_required' in str(self.view_func):
return True
return False
class CustomURLPattern:
def __init__(self, raw, prefix='/'):
self.raw = raw
self.prefix = prefix
self.full_path = f'{self.prefix}{self.raw.pattern}'
self.view = CustomView(view_func=self.raw.callback)
def __str__(self):
s = f'{self.full_path} -> {self.view.view_path}'
return s
def __repr__(self):
return self.__str__()
class ViewSchemaGenerator:
def __init__(self):
self.resolver = get_resolver()
self.url_patterns = self.get_url_patterns()
def get_url_patterns(self):
return self._extract_url_patterns(self.resolver.url_patterns)
def _extract_url_patterns(self, url_patterns, prefix='/'):
url_pattern_objects = []
for pattern in url_patterns:
if isinstance(pattern, URLResolver):
resolver = pattern
_prefix = f'{prefix}{resolver.pattern}'
patterns = self._extract_url_patterns(resolver.url_patterns, prefix=_prefix)
url_pattern_objects.extend(patterns)
continue
elif isinstance(pattern, URLPattern):
p = CustomURLPattern(raw=pattern, prefix=prefix)
url_pattern_objects.append(p)
else:
log(f'Unknown pattern type: {type(pattern)}')
return url_pattern_objects
def generate(self):
self.write_url_patterns()
self.write_webui_schema()
def write_webui_schema(self):
data = {
'GET': {},
'POST': {}
}
post_schema = {}
for pattern in self.url_patterns:
if pattern.view.requires_auth:
continue
url = pattern.full_path
item = {
'allowIf': 'prelogin',
'query': {
"type": "object",
"properties": {},
"required": [],
"additionalProperties": False
},
'body': {
'type': 'object',
'properties': pattern.view.write_fields_schema,
'required': pattern.view.required_fields,
'additionalProperties': False
}
}
post_schema[url] = item
data['POST'] = post_schema
self.write_to_file(data, 'webui_schema.json')
def write_url_patterns(self):
data = []
for pattern in self.url_patterns:
if pattern.view.requires_auth:
continue
if pattern.view.view_type == 'function':
continue
item = {
'url': pattern.full_path,
'view_path': pattern.view.view_path,
'view_requires_auth': pattern.view.requires_auth,
'view_type': pattern.view.view_type,
}
view_write_fields_schema = pattern.view.write_fields_schema
if view_write_fields_schema:
item['view_write_fields_schema'] = view_write_fields_schema
data.append(item)
self.write_to_file(data, 'all_url_patterns.json')
def write_to_file(self, data, filename):
file_path = os.path.join(OUTPUT_FILE_DIR, filename)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
if __name__ == '__main__':
ViewSchemaGenerator().generate()

View File

@@ -0,0 +1,799 @@
[
{
"url": "/api/v1/users/^service-account-registrations/$",
"view_path": "users.api.service.ServiceAccountRegistrationViewSet",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 128
},
"comment": {
"type": "string"
}
}
},
{
"url": "/api/v1/users/^service-account-registrations\\.(?P<format>[a-z0-9]+)/?$",
"view_path": "users.api.service.ServiceAccountRegistrationViewSet",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 128
},
"comment": {
"type": "string"
}
}
},
{
"url": "/api/v1/users/^service-account-registrations/(?P<pk>[^/.]+)/$",
"view_path": "users.api.service.ServiceAccountRegistrationViewSet",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 128
},
"comment": {
"type": "string"
}
}
},
{
"url": "/api/v1/users/^service-account-registrations/(?P<pk>[^/.]+)\\.(?P<format>[a-z0-9]+)/?$",
"view_path": "users.api.service.ServiceAccountRegistrationViewSet",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 128
},
"comment": {
"type": "string"
}
}
},
{
"url": "/api/v1/terminal/terminal-registrations/",
"view_path": "terminal.api.component.terminal.TerminalRegistrationApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"name": {
"type": "string",
"maxLength": 1024
},
"type": {
"type": "string",
"enum": [
"koko",
"guacamole",
"omnidb",
"xrdp",
"lion",
"core",
"celery",
"magnus",
"razor",
"tinker",
"video_worker",
"chen",
"kael",
"panda",
"nec",
"facelive"
]
},
"comment": {
"type": "string"
}
}
},
{
"url": "/api/v1/terminal/registration/",
"view_path": "terminal.api.component.terminal.TerminalRegistrationApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"name": {
"type": "string",
"maxLength": 1024
},
"type": {
"type": "string",
"enum": [
"koko",
"guacamole",
"omnidb",
"xrdp",
"lion",
"core",
"celery",
"magnus",
"razor",
"tinker",
"video_worker",
"chen",
"kael",
"panda",
"nec",
"facelive"
]
},
"comment": {
"type": "string"
}
}
},
{
"url": "/api/v1/settings/logo/",
"view_path": "settings.api.settings.SettingsLogoApi",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/v1/settings/public/open/",
"view_path": "settings.api.public.OpenPublicSettingApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"XPACK_ENABLED": {
"type": "boolean"
},
"INTERFACE": {
"type": "object",
"properties": {}
},
"LANGUAGES": {
"type": "array",
"properties": {}
}
}
},
{
"url": "/api/v1/settings/i18n/<str:name>/",
"view_path": "settings.api.i18n.ComponentI18nApi",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/v1/settings/client/versions/",
"view_path": "settings.api.settings.ClientVersionView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/v1/authentication/face/context/",
"view_path": "authentication.api.face.FaceContextApi",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/v1/authentication/auth/",
"view_path": "authentication.api.token.TokenCreateApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"public_key": {
"type": "string"
}
}
},
{
"url": "/api/v1/authentication/tokens/",
"view_path": "authentication.api.token.TokenCreateApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"public_key": {
"type": "string"
}
}
},
{
"url": "/api/v1/authentication/mfa/verify/",
"view_path": "authentication.api.mfa.MFAChallengeVerifyApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"type": {
"type": "string"
},
"code": {
"type": "string"
}
}
},
{
"url": "/api/v1/authentication/mfa/challenge/",
"view_path": "authentication.api.mfa.MFAChallengeVerifyApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"type": {
"type": "string"
},
"code": {
"type": "string"
}
}
},
{
"url": "/api/v1/authentication/mfa/select/",
"view_path": "authentication.api.mfa.MFASendCodeApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"type": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
{
"url": "/api/v1/authentication/mfa/send-code/",
"view_path": "authentication.api.mfa.MFASendCodeApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"type": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
{
"url": "/api/v1/authentication/password/reset-code/",
"view_path": "authentication.api.password.UserResetPasswordSendCodeApi",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"form_type": {
"type": "string",
"enum": [
"sms",
"email"
]
},
"email": {
"type": "string"
},
"sms": {
"type": "string"
}
}
},
{
"url": "/api/v1/authentication/login-confirm-ticket/status/",
"view_path": "authentication.api.login_confirm.TicketStatusApi",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/v1/authentication/user-session/",
"view_path": "authentication.api.session.UserSessionApi",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/v1/prometheus/metrics/",
"view_path": "jumpserver.api.health.PrometheusMetricsApi",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/health/",
"view_path": "jumpserver.api.health.HealthCheckView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/api/v1/health/",
"view_path": "jumpserver.api.health.HealthCheckView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/login/",
"view_path": "authentication.views.login.UserLoginView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"auto_login": {
"type": "boolean"
},
"captcha": {
"type": "string"
},
"code": {
"type": "string"
},
"mfa_type": {
"type": "string"
},
"challenge": {
"type": "string"
}
}
},
{
"url": "/core/auth/login/mfa/",
"view_path": "authentication.views.mfa.UserLoginMFAView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"code": {
"type": "string"
},
"mfa_type": {
"type": "string"
}
}
},
{
"url": "/core/auth/login/wait-confirm/",
"view_path": "authentication.views.login.UserLoginWaitConfirmView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/login/mfa/face/capture/",
"view_path": "authentication.views.mfa.UserLoginMFAFaceView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"code": {
"type": "string"
}
}
},
{
"url": "/core/auth/login/guard/",
"view_path": "authentication.views.login.UserLoginGuardView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/logout/",
"view_path": "authentication.views.login.UserLogoutView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/password/forget/previewing/",
"view_path": "users.views.profile.reset.UserForgotPasswordPreviewingView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"captcha": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
{
"url": "/core/auth/password/forgot/",
"view_path": "users.views.profile.reset.UserForgotPasswordView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"email": {
"type": "string"
},
"country_code": {
"type": "string"
},
"sms": {
"type": "string",
"description": "The phone number must contain an area code, for example, +86"
},
"code": {
"type": "string"
},
"form_type": {
"type": "string"
}
}
},
{
"url": "/core/auth/password/reset/",
"view_path": "users.views.profile.reset.UserResetPasswordView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"new_password": {
"type": "string"
},
"confirm_password": {
"type": "string"
}
}
},
{
"url": "/core/auth/password/verify/",
"view_path": "users.views.profile.password.UserVerifyPasswordView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"password": {
"type": "string"
}
}
},
{
"url": "/core/auth/wecom/bind/start/",
"view_path": "authentication.views.wecom.WeComEnableStartView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"password": {
"type": "string"
}
}
},
{
"url": "/core/auth/wecom/qr/login/",
"view_path": "authentication.views.wecom.WeComQRLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/wecom/qr/login/callback/",
"view_path": "authentication.views.wecom.WeComQRLoginCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/wecom/oauth/login/",
"view_path": "authentication.views.wecom.WeComOAuthLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/wecom/oauth/login/callback/",
"view_path": "authentication.views.wecom.WeComOAuthLoginCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/dingtalk/bind/start/",
"view_path": "authentication.views.dingtalk.DingTalkEnableStartView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"password": {
"type": "string"
}
}
},
{
"url": "/core/auth/dingtalk/qr/login/",
"view_path": "authentication.views.dingtalk.DingTalkQRLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/dingtalk/qr/login/callback/",
"view_path": "authentication.views.dingtalk.DingTalkQRLoginCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/dingtalk/oauth/login/",
"view_path": "authentication.views.dingtalk.DingTalkOAuthLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/dingtalk/oauth/login/callback/",
"view_path": "authentication.views.dingtalk.DingTalkOAuthLoginCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/feishu/bind/start/",
"view_path": "authentication.views.feishu.FeiShuEnableStartView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"password": {
"type": "string"
}
}
},
{
"url": "/core/auth/feishu/qr/login/",
"view_path": "authentication.views.feishu.FeiShuQRLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/feishu/qr/login/callback/",
"view_path": "authentication.views.feishu.FeiShuQRLoginCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/lark/bind/start/",
"view_path": "authentication.views.lark.LarkEnableStartView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"password": {
"type": "string"
}
}
},
{
"url": "/core/auth/lark/qr/login/",
"view_path": "authentication.views.lark.LarkQRLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/lark/qr/login/callback/",
"view_path": "authentication.views.lark.LarkQRLoginCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/slack/bind/start/",
"view_path": "authentication.views.slack.SlackEnableStartView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"password": {
"type": "string"
}
}
},
{
"url": "/core/auth/slack/qr/login/",
"view_path": "authentication.views.slack.SlackQRLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/slack/qr/login/callback/",
"view_path": "authentication.views.slack.SlackQRLoginCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/profile/otp/enable/start/",
"view_path": "users.views.profile.otp.UserOtpEnableStartView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/profile/otp/enable/install-app/",
"view_path": "users.views.profile.otp.UserOtpEnableInstallAppView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/profile/otp/enable/bind/",
"view_path": "users.views.profile.otp.UserOtpEnableBindView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"otp_code": {
"type": "string"
}
}
},
{
"url": "/core/auth/profile/face/enable/",
"view_path": "users.views.profile.face.UserFaceEnableView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"code": {
"type": "string"
}
}
},
{
"url": "/core/auth/profile/face/disable/",
"view_path": "users.views.profile.face.UserFaceDisableView",
"view_requires_auth": false,
"view_type": "class",
"view_write_fields_schema": {
"code": {
"type": "string"
}
}
},
{
"url": "/core/auth/cas/login/",
"view_path": "authentication.backends.cas.views.CASLoginView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/cas/logout/",
"view_path": "django_cas_ng.views.LogoutView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/cas/callback/",
"view_path": "django_cas_ng.views.CallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/openid/login/",
"view_path": "authentication.backends.oidc.views.OIDCAuthRequestView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/openid/callback/",
"view_path": "authentication.backends.oidc.views.OIDCAuthCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/openid/logout/",
"view_path": "authentication.backends.oidc.views.OIDCEndSessionView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/saml2/login/",
"view_path": "authentication.backends.saml2.views.Saml2AuthRequestView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/saml2/logout/",
"view_path": "authentication.backends.saml2.views.Saml2EndSessionView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/saml2/callback/",
"view_path": "authentication.backends.saml2.views.Saml2AuthCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/saml2/metadata/",
"view_path": "authentication.backends.saml2.views.Saml2AuthMetadataView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/oauth2/login/",
"view_path": "authentication.backends.oauth2.views.OAuth2AuthRequestView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/oauth2/callback/",
"view_path": "authentication.backends.oauth2.views.OAuth2AuthCallbackView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/oauth2/logout/",
"view_path": "authentication.backends.oauth2.views.OAuth2EndSessionView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/oauth2-provider/token/",
"view_path": "oauth2_provider.views.base.TokenView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/oauth2-provider/revoke/",
"view_path": "oauth2_provider.views.base.RevokeTokenView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/auth/oauth2-provider/.well-known/oauth-authorization-server",
"view_path": "authentication.backends.oauth2_provider.views.OAuthAuthorizationServerView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/reports/export-pdf/",
"view_path": "reports.views.ExportPdfView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/reports/send-mail/",
"view_path": "reports.views.SendMailView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/tickets/direct-approve/<str:token>/",
"view_path": "tickets.views.approve.TicketDirectApproveView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/common/flash-message/",
"view_path": "common.views.msg.FlashMessageMsgView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/download/",
"view_path": "jumpserver.views.other.ResourceDownload",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/redirect/confirm/",
"view_path": "jumpserver.views.other.RedirectConfirm",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/i18n/<str:lang>/",
"view_path": "jumpserver.views.other.I18NView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/media/^(?P<path>.*)$",
"view_path": "private_storage.views.PrivateStorageView",
"view_requires_auth": false,
"view_type": "class"
},
{
"url": "/core/jsi18n/",
"view_path": "django.views.i18n.JavaScriptCatalog",
"view_requires_auth": false,
"view_type": "class"
}
]

File diff suppressed because it is too large Load Diff