From bca128ec39e5a3393c9e03c70d9f1ffdc13c4934 Mon Sep 17 00:00:00 2001 From: wxg0103 <727495428@qq.com> Date: Sun, 27 Apr 2025 16:26:40 +0800 Subject: [PATCH] feat: support create user --- .../constants/exception_code_constants.py | 43 ++++++++++ apps/common/constants/permission_constants.py | 9 +- apps/common/models/db_model_manage.py | 35 ++++++++ apps/common/models/handle/base_handle.py | 15 ++++ .../handle/impl/default_base_model_handle.py | 14 ++++ apps/common/utils/common.py | 22 +++++ apps/users/api/user.py | 6 +- apps/users/serializers/user.py | 84 ++++++++++++++++++- apps/users/urls.py | 8 +- apps/users/views/user.py | 16 +++- 10 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 apps/common/constants/exception_code_constants.py create mode 100644 apps/common/models/db_model_manage.py create mode 100644 apps/common/models/handle/base_handle.py create mode 100644 apps/common/models/handle/impl/default_base_model_handle.py diff --git a/apps/common/constants/exception_code_constants.py b/apps/common/constants/exception_code_constants.py new file mode 100644 index 000000000..821318d23 --- /dev/null +++ b/apps/common/constants/exception_code_constants.py @@ -0,0 +1,43 @@ +# coding=utf-8 +""" + @project: qabot + @Author:虎 + @file: exception_code_constants.py + @date:2023/9/4 14:09 + @desc: 异常常量类 +""" +from enum import Enum + +from common.exception.app_exception import AppApiException +from django.utils.translation import gettext_lazy as _ + + +class ExceptionCodeConstantsValue: + def __init__(self, code, message): + self.code = code + self.message = message + + def get_message(self): + return self.message + + def get_code(self): + return self.code + + def to_app_api_exception(self): + return AppApiException(code=self.code, message=self.message) + + +class ExceptionCodeConstants(Enum): + INCORRECT_USERNAME_AND_PASSWORD = ExceptionCodeConstantsValue(1000, _('The username or password is incorrect')) + NOT_AUTHENTICATION = ExceptionCodeConstantsValue(1001, _('Please log in first and bring the user Token')) + EMAIL_SEND_ERROR = ExceptionCodeConstantsValue(1002, _('Email sending failed')) + EMAIL_FORMAT_ERROR = ExceptionCodeConstantsValue(1003, _('Email format error')) + EMAIL_IS_EXIST = ExceptionCodeConstantsValue(1004, _('The email has been registered, please log in directly')) + EMAIL_IS_NOT_EXIST = ExceptionCodeConstantsValue(1005, _('The email is not registered, please register first')) + CODE_ERROR = ExceptionCodeConstantsValue(1005, + _('The verification code is incorrect or the verification code has expired')) + USERNAME_IS_EXIST = ExceptionCodeConstantsValue(1006, _('The username has been registered, please log in directly')) + USERNAME_ERROR = ExceptionCodeConstantsValue(1006, + _('The username cannot be empty and must be between 6 and 20 characters long.')) + PASSWORD_NOT_EQ_RE_PASSWORD = ExceptionCodeConstantsValue(1007, + _('Password and confirmation password are inconsistent')) diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index 592174114..6a61574ad 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -111,6 +111,8 @@ class PermissionConstants(Enum): """ USER_READ = Permission(group=Group.USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER]) + USER_CREATE = Permission(group=Group.USER, operate=Operate.CREATE, + role_list=[RoleConstants.ADMIN, RoleConstants.USER]) USER_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN]) USER_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN]) @@ -153,11 +155,12 @@ class PermissionConstants(Enum): KNOWLEDGE_MODULE_EDIT = Permission(group=Group.KNOWLEDGE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER]) KNOWLEDGE_MODULE_DELETE = Permission(group=Group.KNOWLEDGE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, - RoleConstants.USER]) + RoleConstants.USER]) KNOWLEDGE_READ = Permission(group=Group.KNOWLEDGE, operate=Operate.READ, role_list=[RoleConstants.ADMIN, - RoleConstants.USER]) + RoleConstants.USER]) KNOWLEDGE_CREATE = Permission(group=Group.KNOWLEDGE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, - RoleConstants.USER]) + RoleConstants.USER]) + def get_workspace_application_permission(self): return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate, resource_path= diff --git a/apps/common/models/db_model_manage.py b/apps/common/models/db_model_manage.py new file mode 100644 index 000000000..80ce0f55b --- /dev/null +++ b/apps/common/models/db_model_manage.py @@ -0,0 +1,35 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: db_model_manage.py + @date:2024/7/22 17:00 + @desc: +""" +from importlib import import_module +from django.conf import settings + + +def new_instance_by_class_path(class_path: str): + parts = class_path.rpartition('.') + package_path = parts[0] + class_name = parts[2] + module = import_module(package_path) + HandlerClass = getattr(module, class_name) + return HandlerClass() + + +class DBModelManage: + model_dict = {} + + @staticmethod + def get_model(model_name): + return DBModelManage.model_dict.get(model_name) + + @staticmethod + def init(): + handles = [new_instance_by_class_path(class_path) for class_path in + (settings.MODEL_HANDLES if hasattr(settings, 'MODEL_HANDLES') else [])] + for h in handles: + model_dict = h.get_model_dict() + DBModelManage.model_dict = {**DBModelManage.model_dict, **model_dict} diff --git a/apps/common/models/handle/base_handle.py b/apps/common/models/handle/base_handle.py new file mode 100644 index 000000000..17389673e --- /dev/null +++ b/apps/common/models/handle/base_handle.py @@ -0,0 +1,15 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: base_handle.py + @date:2024/7/22 17:02 + @desc: +""" +from abc import ABC, abstractmethod + + +class IBaseModelHandle(ABC): + @abstractmethod + def get_model_dict(self): + pass diff --git a/apps/common/models/handle/impl/default_base_model_handle.py b/apps/common/models/handle/impl/default_base_model_handle.py new file mode 100644 index 000000000..b1ed7051a --- /dev/null +++ b/apps/common/models/handle/impl/default_base_model_handle.py @@ -0,0 +1,14 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: default_base_model_handle.py + @date:2024/7/22 17:06 + @desc: +""" +from common.models.handle.base_handle import IBaseModelHandle + + +class DefaultBaseModelHandle(IBaseModelHandle): + def get_model_dict(self): + return {} diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 680ec602f..48acd33d6 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -17,10 +17,12 @@ from functools import reduce from typing import List, Dict from django.core.files.uploadedfile import InMemoryUploadedFile +from django.db.models import QuerySet from django.utils.translation import gettext as _ from pydub import AudioSegment from ..exception.app_exception import AppApiException +from ..models.db_model_manage import DBModelManage def password_encrypt(row_password): @@ -211,3 +213,23 @@ def query_params_to_single_dict(query_params: Dict): filter(lambda item: item is not None, [({key: value} if value is not None and len(value) > 0 else None) for key, value in query_params.items()])), {}) + + +def valid_license(model=None, count=None, message=None): + def inner(func): + def run(*args, **kwargs): + xpack_cache = DBModelManage.get_model('xpack_cache') + is_license_valid = xpack_cache.get('XPACK_LICENSE_IS_VALID', False) if xpack_cache is not None else False + record_count = QuerySet(model).count() + + if not is_license_valid and record_count >= count: + error_message = message or _( + 'Limit {count} exceeded, please contact us (https://fit2cloud.com/).').format( + count=count) + raise AppApiException(400, error_message) + + return func(*args, **kwargs) + + return run + + return inner diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 57e87d5fd..415ea4913 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -11,7 +11,7 @@ from drf_spectacular.utils import OpenApiParameter from common.mixins.api_mixin import APIMixin from common.result import ResultSerializer -from users.serializers.user import UserProfileResponse +from users.serializers.user import UserProfileResponse, CreateUserSerializer class ApiUserProfileResponse(ResultSerializer): @@ -25,6 +25,10 @@ class UserProfileAPI(APIMixin): def get_response(): return ApiUserProfileResponse + @staticmethod + def get_request(): + return CreateUserSerializer + class TestWorkspacePermissionUserApi(APIMixin): @staticmethod diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 4c64f4917..cf2e50440 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -6,18 +6,35 @@ @date:2025/4/14 19:18 @desc: """ -from rest_framework import serializers +import re +from django.db import transaction +from django.db.models import QuerySet, Q +from rest_framework import serializers +import uuid_utils.compat as uuid +from common.constants.exception_code_constants import ExceptionCodeConstants +from common.constants.permission_constants import RoleConstants +from common.utils.common import valid_license, password_encrypt from users.models import User +from django.utils.translation import gettext_lazy as _ +from django.core import validators class UserProfileResponse(serializers.ModelSerializer): - is_edit_password = serializers.BooleanField(required=True, label="是否修改密码") - permissions = serializers.ListField(required=True, label="权限") + is_edit_password = serializers.BooleanField(required=True, label=_('Is Edit Password')) + permissions = serializers.ListField(required=True, label=_('permissions')) class Meta: model = User - fields = ['id', 'username', 'email', 'role', 'permissions', 'language', 'is_edit_password'] + fields = ['id', 'username', 'nick_name', 'email', 'role', 'permissions', 'language', 'is_edit_password'] + + +class CreateUserSerializer(serializers.ModelSerializer): + username = serializers.CharField(required=True, label=_('Username')) + password = serializers.CharField(required=True, label=_('Password')) + email = serializers.EmailField(required=True, label=_('Email')) + nick_name = serializers.CharField(required=False, label=_('Nick name')) + phone = serializers.CharField(required=False, label=_('Phone')) class UserProfileSerializer(serializers.Serializer): @@ -30,8 +47,67 @@ class UserProfileSerializer(serializers.Serializer): """ return {'id': user.id, 'username': user.username, + 'nick_name': user.nick_name, 'email': user.email, 'role': user.role, 'permissions': [str(p) for p in []], 'is_edit_password': user.password == 'd880e722c47a34d8e9fce789fc62389d' if user.role == 'ADMIN' else False, 'language': user.language} + + +class UserManageSerializer(serializers.Serializer): + class UserInstance(serializers.Serializer): + email = serializers.EmailField( + required=True, + label=_("Email"), + validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message, + code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)]) + + username = serializers.CharField(required=True, + label=_("Username"), + max_length=20, + min_length=6, + validators=[ + validators.RegexValidator(regex=re.compile("^.{6,20}$"), + message=_( + 'Username must be 6-20 characters long')) + ]) + password = serializers.CharField(required=True, label=_("Password"), max_length=20, min_length=6, + validators=[validators.RegexValidator(regex=re.compile( + "^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~.()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)" + "(?![0-9_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9_!@#$%^&*`~.()-+=]{6,20}$") + , message=_( + "The password must be 6-20 characters long and must be a combination of letters, numbers, and special characters."))]) + + nick_name = serializers.CharField(required=False, label=_("Nick name"), max_length=64, + allow_null=True, allow_blank=True) + phone = serializers.CharField(required=False, label=_("Phone"), max_length=20, + allow_null=True, allow_blank=True) + + def is_valid(self, *, raise_exception=True): + super().is_valid(raise_exception=True) + username = self.data.get('username') + email = self.data.get('email') + u = QuerySet(User).filter(Q(username=username) | Q(email=email)).first() + if u is not None: + if u.email == email: + raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception() + if u.username == username: + raise ExceptionCodeConstants.USERNAME_IS_EXIST.value.to_app_api_exception() + + @valid_license(model=User, count=2, + message=_( + 'The community version supports up to 2 users. If you need more users, please contact us (https://fit2cloud.com/).')) + @transaction.atomic + def save(self, instance, with_valid=True): + if with_valid: + UserManageSerializer.UserInstance(data=instance).is_valid(raise_exception=True) + + user = User(id=uuid.uuid7(), email=instance.get('email'), + phone="" if instance.get('phone') is None else instance.get('phone'), + nick_name="" if instance.get('nick_name') is None else instance.get('nick_name') + , username=instance.get('username'), password=password_encrypt(instance.get('password')), + role=RoleConstants.USER.name, source="LOCAL", + is_active=True) + user.save() + return UserProfileSerializer(user).data diff --git a/apps/users/urls.py b/apps/users/urls.py index f5d99e380..43ff85747 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -9,5 +9,11 @@ urlpatterns = [ path('user/captcha', views.CaptchaView.as_view(), name='captcha'), path('user/test', views.TestPermissionsUserView.as_view(), name="test"), path('workspace//user/profile', views.TestWorkspacePermissionUserView.as_view(), - name="test_workspace_id_permission") + name="test_workspace_id_permission"), + path("user_manage", views.UserManage.as_view(), name="user_manage"), + # path("user_manage/", views.UserManage.Operate.as_view(), name="user_manage_operate"), + # path("user_manage//re_password", views.UserManage.RePassword.as_view(), + # name="user_manage_re_password"), + # path("user_manage//", views.UserManage.Page.as_view(), + # name="user_manage_re_password"), ] diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 94e1a1796..f67675ab7 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -16,7 +16,7 @@ from common.auth.authentication import has_permissions from common.constants.permission_constants import PermissionConstants, Permission, Group, Operate from common.result import result from users.api.user import UserProfileAPI, TestWorkspacePermissionUserApi -from users.serializers.user import UserProfileSerializer +from users.serializers.user import UserProfileSerializer, UserManageSerializer class UserProfileView(APIView): @@ -56,3 +56,17 @@ class TestWorkspacePermissionUserView(APIView): @has_permissions(PermissionConstants.USER_EDIT.get_workspace_permission()) def get(self, request: Request, workspace_id): return result.success(UserProfileSerializer().profile(request.user)) + + +class UserManage(APIView): + authentication_classes = [TokenAuth] + + @extend_schema(methods=['POST'], + description=_("Create user"), + operation_id=_("Create user"), + tags=[_("User management")], + request=UserProfileAPI.get_request(), + responses=UserProfileAPI.get_response()) + @has_permissions(PermissionConstants.USER_CREATE) + def post(self, request: Request): + return result.success(UserManageSerializer().save(request.data))