feat: 接口校验 (#721)

feat: 接口校验
This commit is contained in:
shaohuzhang1 2024-07-09 13:44:27 +08:00 committed by GitHub
parent ed526c3bf3
commit 60e65a8b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 256 additions and 14 deletions

View File

@ -32,6 +32,7 @@ from common.db.search import get_dynamics_model, native_search, native_page_sear
from common.db.sql_execute import select_list from common.db.sql_execute import select_list
from common.exception.app_exception import AppApiException, NotFound404, AppUnauthorizedFailed from common.exception.app_exception import AppApiException, NotFound404, AppUnauthorizedFailed
from common.field.common import UploadedImageField from common.field.common import UploadedImageField
from common.util.common import valid_license
from common.util.field_message import ErrMessage from common.util.field_message import ErrMessage
from common.util.file_util import get_file_content from common.util.file_util import get_file_content
from dataset.models import DataSet, Document, Image from dataset.models import DataSet, Document, Image
@ -329,6 +330,8 @@ class ApplicationSerializer(serializers.Serializer):
class Create(serializers.Serializer): class Create(serializers.Serializer):
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id")) user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
@valid_license(model=Application, count=5,
message='社区版最多支持 5 个应用如需拥有更多应用请联系我们https://fit2cloud.com/)。')
@transaction.atomic @transaction.atomic
def insert(self, application: Dict): def insert(self, application: Dict):
application_type = application.get('type') application_type = application.get('type')

View File

@ -10,6 +10,11 @@ import importlib
from functools import reduce from functools import reduce
from typing import Dict, List from typing import Dict, List
from django.conf import settings
from django.db.models import QuerySet
from ..exception.app_exception import AppApiException
def sub_array(array: List, item_num=10): def sub_array(array: List, item_num=10):
result = [] result = []
@ -66,3 +71,19 @@ def post(post_function):
return run return run
return inner return inner
def valid_license(model=None, count=None, message=None):
def inner(func):
def run(*args, **kwargs):
if (not (settings.XPACK_LICENSE_IS_VALID if hasattr(settings,
'XPACK_LICENSE_IS_VALID') else None)
and QuerySet(
model).count() >= count):
error = message or f'超出限制{count},请联系我们https://fit2cloud.com/)。'
raise AppApiException(400, error)
return func(*args, **kwargs)
return run
return inner

View File

@ -15,7 +15,7 @@ from functools import reduce
from typing import Dict, List from typing import Dict, List
from urllib.parse import urlparse from urllib.parse import urlparse
import xlwt from django.conf import settings
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core import validators from django.core import validators
from django.db import transaction, models from django.db import transaction, models
@ -31,7 +31,7 @@ from common.db.sql_execute import select_list
from common.event import ListenerManagement, SyncWebDatasetArgs from common.event import ListenerManagement, SyncWebDatasetArgs
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from common.mixins.api_mixin import ApiMixin from common.mixins.api_mixin import ApiMixin
from common.util.common import post, flat_map from common.util.common import post, flat_map, valid_license
from common.util.field_message import ErrMessage from common.util.field_message import ErrMessage
from common.util.file_util import get_file_content from common.util.file_util import get_file_content
from common.util.fork import ChildLink, Fork from common.util.fork import ChildLink, Fork
@ -368,6 +368,8 @@ class DataSetSerializers(serializers.ModelSerializer):
dataset_instance = {'name': instance.get('name'), 'desc': instance.get('desc'), 'documents': document_list} dataset_instance = {'name': instance.get('name'), 'desc': instance.get('desc'), 'documents': document_list}
return self.save(dataset_instance, with_valid=True) return self.save(dataset_instance, with_valid=True)
@valid_license(model=DataSet, count=50,
message='社区版最多支持 50 个知识库如需拥有更多知识库请联系我们https://fit2cloud.com/)。')
@post(post_function=post_embedding_dataset) @post(post_function=post_embedding_dataset)
@transaction.atomic @transaction.atomic
def save(self, instance: Dict, with_valid=True): def save(self, instance: Dict, with_valid=True):

View File

@ -0,0 +1,50 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file valid_serializers.py
@date2024/7/8 18:00
@desc:
"""
import re
from django.core import validators
from django.db.models import QuerySet
from rest_framework import serializers
from application.models import Application
from common.exception.app_exception import AppApiException
from common.util.field_message import ErrMessage
from dataset.models import DataSet
from smartdoc import settings
from users.models import User
model_message_dict = {
'dataset': {'model': DataSet, 'count': 50,
'message': '社区版最多支持 50 个知识库如需拥有更多知识库请联系我们https://fit2cloud.com/)。'},
'application': {'model': Application, 'count': 5,
'message': '社区版最多支持 5 个应用如需拥有更多应用请联系我们https://fit2cloud.com/)。'},
'user': {'model': User, 'count': 2,
'message': '社区版最多支持 2 个用户如需拥有更多用户请联系我们https://fit2cloud.com/)。'}
}
class ValidSerializer(serializers.Serializer):
valid_type = serializers.CharField(required=True, error_messages=ErrMessage.char("类型"), validators=[
validators.RegexValidator(regex=re.compile("^application|dataset|user$"),
message="类型只支持:application|dataset|user", code=500)
])
valid_count = serializers.IntegerField(required=True, error_messages=ErrMessage.integer("校验数量"))
def valid(self, is_valid=True):
if is_valid:
self.is_valid(raise_exception=True)
model_value = model_message_dict.get(self.data.get('valid_type'))
if not (settings.XPACK_LICENSE_IS_VALID if hasattr(settings,
'XPACK_LICENSE_IS_VALID') else None):
if self.data.get('valid_count') != model_value.get('count'):
raise AppApiException(400, model_value.get('message'))
if QuerySet(
model_value.get('model')).count() >= model_value.get('count'):
raise AppApiException(400, model_value.get('message'))
return True

View File

@ -0,0 +1,27 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file valid_api.py
@date2024/7/8 17:52
@desc:
"""
from drf_yasg import openapi
from common.mixins.api_mixin import ApiMixin
class ValidApi(ApiMixin):
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='valid_type',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='校验类型:application|dataset|user'),
openapi.Parameter(name='valid_count',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='校验数量')
]

View File

@ -17,6 +17,7 @@ urlpatterns = [
path('model', views.Model.as_view(), name='model'), path('model', views.Model.as_view(), name='model'),
path('model/<str:model_id>', views.Model.Operate.as_view(), name='model/operate'), path('model/<str:model_id>', views.Model.Operate.as_view(), name='model/operate'),
path('model/<str:model_id>/meta', views.Model.ModelMeta.as_view(), name='model/operate/meta'), path('model/<str:model_id>/meta', views.Model.ModelMeta.as_view(), name='model/operate/meta'),
path('email_setting', views.SystemSetting.Email.as_view(), name='email_setting') path('email_setting', views.SystemSetting.Email.as_view(), name='email_setting'),
path('valid/<str:valid_type>/<int:valid_count>', views.Valid.as_view())
] ]

View File

@ -9,3 +9,4 @@
from .Team import * from .Team import *
from .model import * from .model import *
from .system_setting import * from .system_setting import *
from .valid import *

View File

@ -0,0 +1,32 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file valid.py
@date2024/7/8 17:50
@desc:
"""
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.views import APIView
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import RoleConstants
from common.response import result
from setting.serializers.valid_serializers import ValidSerializer
from setting.swagger_api.valid_api import ValidApi
class Valid(APIView):
authentication_classes = [TokenAuth]
@action(methods=['GET'], detail=False)
@swagger_auto_schema(operation_summary="获取校验结果",
operation_id="获取校验结果",
manual_parameters=ValidApi.get_request_params_api(),
responses=result.get_default_response()
, tags=["校验"])
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
def get(self, request: Request, valid_type: str, valid_count: int):
return result.success(ValidSerializer(data={'valid_type': valid_type, 'valid_count': valid_count}).valid())

View File

@ -12,6 +12,7 @@ import random
import re import re
import uuid import uuid
from django.conf import settings
from django.core import validators, signing, cache from django.core import validators, signing, cache
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.mail.backends.smtp import EmailBackend from django.core.mail.backends.smtp import EmailBackend
@ -29,6 +30,7 @@ from common.event import ListenerManagement
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from common.mixins.api_mixin import ApiMixin from common.mixins.api_mixin import ApiMixin
from common.response.result import get_api_response from common.response.result import get_api_response
from common.util.common import valid_license
from common.util.field_message import ErrMessage from common.util.field_message import ErrMessage
from common.util.lock import lock from common.util.lock import lock
from dataset.models import DataSet, Document, Paragraph, Problem, ProblemParagraphMapping from dataset.models import DataSet, Document, Paragraph, Problem, ProblemParagraphMapping
@ -43,7 +45,9 @@ class SystemSerializer(ApiMixin, serializers.Serializer):
@staticmethod @staticmethod
def get_profile(): def get_profile():
version = os.environ.get('MAXKB_VERSION') version = os.environ.get('MAXKB_VERSION')
return {'version': version} return {'version': version, 'IS_XPACK': hasattr(settings, 'XPACK_LICENSE_IS_VALID'),
'XPACK_LICENSE_IS_VALID': (settings.XPACK_LICENSE_IS_VALID if hasattr(settings,
'XPACK_LICENSE_IS_VALID') else False)}
@staticmethod @staticmethod
def get_response_body_api(): def get_response_body_api():
@ -174,6 +178,8 @@ class RegisterSerializer(ApiMixin, serializers.Serializer):
return True return True
@valid_license(model=User, count=2,
message='社区版最多支持 2 个用户如需拥有更多用户请联系我们https://fit2cloud.com/)。')
@transaction.atomic @transaction.atomic
def save(self, **kwargs): def save(self, **kwargs):
m = User( m = User(
@ -681,6 +687,8 @@ class UserManageSerializer(serializers.Serializer):
if self.data.get('password') != self.data.get('re_password'): if self.data.get('password') != self.data.get('re_password'):
raise ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.to_app_api_exception() raise ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.to_app_api_exception()
@valid_license(model=User, count=2,
message='社区版最多支持 2 个用户如需拥有更多用户请联系我们https://fit2cloud.com/)。')
@transaction.atomic @transaction.atomic
def save(self, instance, with_valid=True): def save(self, instance, with_valid=True):
if with_valid: if with_valid:

View File

@ -23,6 +23,8 @@ interface User {
* *
*/ */
is_edit_password?: boolean is_edit_password?: boolean
IS_XPACK?: boolean
XPACK_LICENSE_IS_VALID?: boolean
} }
interface LoginRequest { interface LoginRequest {

View File

@ -131,6 +131,19 @@ const getVersion: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) =
return get('/profile', undefined, loading) return get('/profile', undefined, loading)
} }
/**
*
* @param valid_type 校验类型: application|dataset|user
* @param valid_count 校验数量: 5 | 50 | 2
*/
const getValid: (
valid_type: string,
valid_count: number,
loading?: Ref<boolean>
) => Promise<Result<any>> = (valid_type, valid_count, loading) => {
return get(`/valid/${valid_type}/${valid_count}`, undefined, loading)
}
export default { export default {
login, login,
register, register,
@ -142,5 +155,6 @@ export default {
resetCurrentUserPassword, resetCurrentUserPassword,
logout, logout,
getUserList, getUserList,
getVersion getVersion,
getValid
} }

View File

@ -2,3 +2,15 @@ export enum DeviceType {
Mobile = 'Mobile', Mobile = 'Mobile',
Desktop = 'Desktop' Desktop = 'Desktop'
} }
export enum ValidType {
Application = 'application',
Dataset = 'dataset',
User = 'user'
}
export enum ValidCount {
Application = 5,
Dataset = 50,
User = 2
}

View File

@ -40,10 +40,12 @@ instance.interceptors.response.use(
(response: any) => { (response: any) => {
if (response.data) { if (response.data) {
if (response.data.code !== 200 && !(response.data instanceof Blob)) { if (response.data.code !== 200 && !(response.data instanceof Blob)) {
if (!response.config.url.includes('/valid')) {
MsgError(response.data.message) MsgError(response.data.message)
return Promise.reject(response.data) return Promise.reject(response.data)
} }
} }
}
return response return response
}, },
(err: any) => { (err: any) => {

View File

@ -1,5 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { DeviceType } from '@/enums/common' import { DeviceType, ValidType } from '@/enums/common'
import type { Ref } from 'vue'
import userApi from '@/api/user'
export interface commonTypes { export interface commonTypes {
breadcrumb: any breadcrumb: any
@ -32,6 +34,18 @@ const useCommonStore = defineStore({
}, },
isMobile() { isMobile() {
return this.device === DeviceType.Mobile return this.device === DeviceType.Mobile
},
async asyncGetValid(valid_type: ValidType, valid_count: number, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
userApi
.getValid(valid_type, valid_count, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
} }
} }
}) })

View File

@ -8,6 +8,7 @@ export interface userStateTypes {
token: any token: any
version?: string version?: string
accessToken?: string accessToken?: string
isXPack: false
} }
const useUserStore = defineStore({ const useUserStore = defineStore({
@ -16,9 +17,13 @@ const useUserStore = defineStore({
userType: 1, userType: 1,
userInfo: null, userInfo: null,
token: '', token: '',
version: '' version: '',
isXPack: false
}), }),
actions: { actions: {
isEnterprise() {
return this.userInfo?.IS_XPACK && this.userInfo?.XPACK_LICENSE_IS_VALID
},
getToken(): String | null { getToken(): String | null {
if (this.token) { if (this.token) {
return this.token return this.token

View File

@ -36,6 +36,14 @@ export const MsgError = (message: string) => {
}) })
} }
export const MsgAlert = (title: string, description: string, options?: any) => {
const defaultOptions: Object = {
confirmButtonText: '确定',
...options
}
return ElMessageBox.alert(description, title, defaultOptions)
}
/** /**
* *
* @param message: {title, description,type} * @param message: {title, description,type}

View File

@ -66,7 +66,7 @@
<el-button @click.prevent="dialogVisible = false"> <el-button @click.prevent="dialogVisible = false">
{{ $t('views.application.applicationForm.buttons.cancel') }} {{ $t('views.application.applicationForm.buttons.cancel') }}
</el-button> </el-button>
<el-button type="primary" @click="submitHandle(applicationFormRef)"> <el-button type="primary" @click="submitValid(applicationFormRef)">
{{ $t('views.application.applicationForm.buttons.create') }} {{ $t('views.application.applicationForm.buttons.create') }}
</el-button> </el-button>
</span> </span>
@ -79,9 +79,13 @@ import { useRouter, useRoute } from 'vue-router'
import type { ApplicationFormType } from '@/api/type/application' import type { ApplicationFormType } from '@/api/type/application'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import applicationApi from '@/api/application' import applicationApi from '@/api/application'
import { MsgSuccess } from '@/utils/message' import { MsgSuccess, MsgAlert } from '@/utils/message'
import { isWorkFlow } from '@/utils/application' import { isWorkFlow } from '@/utils/application'
import { t } from '@/locales' import { t } from '@/locales'
import useStore from '@/stores'
import { ValidType, ValidCount } from '@/enums/common'
const { common, user } = useStore()
const router = useRouter() const router = useRouter()
const emit = defineEmits(['refresh']) const emit = defineEmits(['refresh'])
@ -169,6 +173,24 @@ const open = () => {
dialogVisible.value = true dialogVisible.value = true
} }
const submitValid = (formEl: FormInstance | undefined) => {
if (user.isEnterprise()) {
submitHandle(formEl)
} else {
common
.asyncGetValid(ValidType.Application, ValidCount.Application, loading)
.then(async (res: any) => {
if (res?.data?.data) {
submitHandle(formEl)
} else {
MsgAlert(
'提示',
'社区版最多支持 5 个应用如需拥有更多应用请联系我们https://fit2cloud.com/)。'
)
}
})
}
}
const submitHandle = async (formEl: FormInstance | undefined) => { const submitHandle = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
await formEl.validate((valid) => { await formEl.validate((valid) => {

View File

@ -83,9 +83,13 @@
import { ref, onMounted, reactive } from 'vue' import { ref, onMounted, reactive } from 'vue'
import UserDialog from './component/UserDialog.vue' import UserDialog from './component/UserDialog.vue'
import UserPwdDialog from './component/UserPwdDialog.vue' import UserPwdDialog from './component/UserPwdDialog.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message' import { MsgSuccess, MsgConfirm, MsgAlert } from '@/utils/message'
import userApi from '@/api/user-manage' import userApi from '@/api/user-manage'
import { datetimeFormat } from '@/utils/time' import { datetimeFormat } from '@/utils/time'
import useStore from '@/stores'
import { ValidType, ValidCount } from '@/enums/common'
const { common, user } = useStore()
const UserDialogRef = ref() const UserDialogRef = ref()
const UserPwdDialogRef = ref() const UserPwdDialogRef = ref()
@ -127,8 +131,22 @@ function editUser(row: any) {
} }
function createUser() { function createUser() {
if (user.isEnterprise()) {
title.value = '创建用户' title.value = '创建用户'
UserDialogRef.value.open() UserDialogRef.value.open()
} else {
common.asyncGetValid(ValidType.User, ValidCount.User, loading).then((res: any) => {
if (res?.data?.data) {
title.value = '创建用户'
UserDialogRef.value.open()
} else {
MsgAlert(
'提示',
'社区版最多支持 2 个用户如需拥有更多用户请联系我们https://fit2cloud.com/)。'
)
}
})
}
} }
function deleteUserManage(row: any) { function deleteUserManage(row: any) {