Skip to content

Commit bca128e

Browse files
committed
feat: support create user
1 parent a91d801 commit bca128e

File tree

10 files changed

+242
-10
lines changed

10 files changed

+242
-10
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# coding=utf-8
2+
"""
3+
@project: qabot
4+
@Author:虎
5+
@file: exception_code_constants.py
6+
@date:2023/9/4 14:09
7+
@desc: 异常常量类
8+
"""
9+
from enum import Enum
10+
11+
from common.exception.app_exception import AppApiException
12+
from django.utils.translation import gettext_lazy as _
13+
14+
15+
class ExceptionCodeConstantsValue:
16+
def __init__(self, code, message):
17+
self.code = code
18+
self.message = message
19+
20+
def get_message(self):
21+
return self.message
22+
23+
def get_code(self):
24+
return self.code
25+
26+
def to_app_api_exception(self):
27+
return AppApiException(code=self.code, message=self.message)
28+
29+
30+
class ExceptionCodeConstants(Enum):
31+
INCORRECT_USERNAME_AND_PASSWORD = ExceptionCodeConstantsValue(1000, _('The username or password is incorrect'))
32+
NOT_AUTHENTICATION = ExceptionCodeConstantsValue(1001, _('Please log in first and bring the user Token'))
33+
EMAIL_SEND_ERROR = ExceptionCodeConstantsValue(1002, _('Email sending failed'))
34+
EMAIL_FORMAT_ERROR = ExceptionCodeConstantsValue(1003, _('Email format error'))
35+
EMAIL_IS_EXIST = ExceptionCodeConstantsValue(1004, _('The email has been registered, please log in directly'))
36+
EMAIL_IS_NOT_EXIST = ExceptionCodeConstantsValue(1005, _('The email is not registered, please register first'))
37+
CODE_ERROR = ExceptionCodeConstantsValue(1005,
38+
_('The verification code is incorrect or the verification code has expired'))
39+
USERNAME_IS_EXIST = ExceptionCodeConstantsValue(1006, _('The username has been registered, please log in directly'))
40+
USERNAME_ERROR = ExceptionCodeConstantsValue(1006,
41+
_('The username cannot be empty and must be between 6 and 20 characters long.'))
42+
PASSWORD_NOT_EQ_RE_PASSWORD = ExceptionCodeConstantsValue(1007,
43+
_('Password and confirmation password are inconsistent'))

apps/common/constants/permission_constants.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ class PermissionConstants(Enum):
111111
"""
112112
USER_READ = Permission(group=Group.USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN,
113113
RoleConstants.USER])
114+
USER_CREATE = Permission(group=Group.USER, operate=Operate.CREATE,
115+
role_list=[RoleConstants.ADMIN, RoleConstants.USER])
114116
USER_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN])
115117
USER_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN])
116118

@@ -153,11 +155,12 @@ class PermissionConstants(Enum):
153155
KNOWLEDGE_MODULE_EDIT = Permission(group=Group.KNOWLEDGE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN,
154156
RoleConstants.USER])
155157
KNOWLEDGE_MODULE_DELETE = Permission(group=Group.KNOWLEDGE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN,
156-
RoleConstants.USER])
158+
RoleConstants.USER])
157159
KNOWLEDGE_READ = Permission(group=Group.KNOWLEDGE, operate=Operate.READ, role_list=[RoleConstants.ADMIN,
158-
RoleConstants.USER])
160+
RoleConstants.USER])
159161
KNOWLEDGE_CREATE = Permission(group=Group.KNOWLEDGE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN,
160-
RoleConstants.USER])
162+
RoleConstants.USER])
163+
161164
def get_workspace_application_permission(self):
162165
return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
163166
resource_path=
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎
5+
@file: db_model_manage.py
6+
@date:2024/7/22 17:00
7+
@desc:
8+
"""
9+
from importlib import import_module
10+
from django.conf import settings
11+
12+
13+
def new_instance_by_class_path(class_path: str):
14+
parts = class_path.rpartition('.')
15+
package_path = parts[0]
16+
class_name = parts[2]
17+
module = import_module(package_path)
18+
HandlerClass = getattr(module, class_name)
19+
return HandlerClass()
20+
21+
22+
class DBModelManage:
23+
model_dict = {}
24+
25+
@staticmethod
26+
def get_model(model_name):
27+
return DBModelManage.model_dict.get(model_name)
28+
29+
@staticmethod
30+
def init():
31+
handles = [new_instance_by_class_path(class_path) for class_path in
32+
(settings.MODEL_HANDLES if hasattr(settings, 'MODEL_HANDLES') else [])]
33+
for h in handles:
34+
model_dict = h.get_model_dict()
35+
DBModelManage.model_dict = {**DBModelManage.model_dict, **model_dict}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎
5+
@file: base_handle.py
6+
@date:2024/7/22 17:02
7+
@desc:
8+
"""
9+
from abc import ABC, abstractmethod
10+
11+
12+
class IBaseModelHandle(ABC):
13+
@abstractmethod
14+
def get_model_dict(self):
15+
pass
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎
5+
@file: default_base_model_handle.py
6+
@date:2024/7/22 17:06
7+
@desc:
8+
"""
9+
from common.models.handle.base_handle import IBaseModelHandle
10+
11+
12+
class DefaultBaseModelHandle(IBaseModelHandle):
13+
def get_model_dict(self):
14+
return {}

apps/common/utils/common.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
from typing import List, Dict
1818

1919
from django.core.files.uploadedfile import InMemoryUploadedFile
20+
from django.db.models import QuerySet
2021
from django.utils.translation import gettext as _
2122
from pydub import AudioSegment
2223

2324
from ..exception.app_exception import AppApiException
25+
from ..models.db_model_manage import DBModelManage
2426

2527

2628
def password_encrypt(row_password):
@@ -211,3 +213,23 @@ def query_params_to_single_dict(query_params: Dict):
211213
filter(lambda item: item is not None, [({key: value} if value is not None and len(value) > 0 else None) for
212214
key, value in
213215
query_params.items()])), {})
216+
217+
218+
def valid_license(model=None, count=None, message=None):
219+
def inner(func):
220+
def run(*args, **kwargs):
221+
xpack_cache = DBModelManage.get_model('xpack_cache')
222+
is_license_valid = xpack_cache.get('XPACK_LICENSE_IS_VALID', False) if xpack_cache is not None else False
223+
record_count = QuerySet(model).count()
224+
225+
if not is_license_valid and record_count >= count:
226+
error_message = message or _(
227+
'Limit {count} exceeded, please contact us (https://fit2cloud.com/).').format(
228+
count=count)
229+
raise AppApiException(400, error_message)
230+
231+
return func(*args, **kwargs)
232+
233+
return run
234+
235+
return inner

apps/users/api/user.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from common.mixins.api_mixin import APIMixin
1313
from common.result import ResultSerializer
14-
from users.serializers.user import UserProfileResponse
14+
from users.serializers.user import UserProfileResponse, CreateUserSerializer
1515

1616

1717
class ApiUserProfileResponse(ResultSerializer):
@@ -25,6 +25,10 @@ class UserProfileAPI(APIMixin):
2525
def get_response():
2626
return ApiUserProfileResponse
2727

28+
@staticmethod
29+
def get_request():
30+
return CreateUserSerializer
31+
2832

2933
class TestWorkspacePermissionUserApi(APIMixin):
3034
@staticmethod

apps/users/serializers/user.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,35 @@
66
@date:2025/4/14 19:18
77
@desc:
88
"""
9-
from rest_framework import serializers
9+
import re
1010

11+
from django.db import transaction
12+
from django.db.models import QuerySet, Q
13+
from rest_framework import serializers
14+
import uuid_utils.compat as uuid
15+
from common.constants.exception_code_constants import ExceptionCodeConstants
16+
from common.constants.permission_constants import RoleConstants
17+
from common.utils.common import valid_license, password_encrypt
1118
from users.models import User
19+
from django.utils.translation import gettext_lazy as _
20+
from django.core import validators
1221

1322

1423
class UserProfileResponse(serializers.ModelSerializer):
15-
is_edit_password = serializers.BooleanField(required=True, label="是否修改密码")
16-
permissions = serializers.ListField(required=True, label="权限")
24+
is_edit_password = serializers.BooleanField(required=True, label=_('Is Edit Password'))
25+
permissions = serializers.ListField(required=True, label=_('permissions'))
1726

1827
class Meta:
1928
model = User
20-
fields = ['id', 'username', 'email', 'role', 'permissions', 'language', 'is_edit_password']
29+
fields = ['id', 'username', 'nick_name', 'email', 'role', 'permissions', 'language', 'is_edit_password']
30+
31+
32+
class CreateUserSerializer(serializers.ModelSerializer):
33+
username = serializers.CharField(required=True, label=_('Username'))
34+
password = serializers.CharField(required=True, label=_('Password'))
35+
email = serializers.EmailField(required=True, label=_('Email'))
36+
nick_name = serializers.CharField(required=False, label=_('Nick name'))
37+
phone = serializers.CharField(required=False, label=_('Phone'))
2138

2239

2340
class UserProfileSerializer(serializers.Serializer):
@@ -30,8 +47,67 @@ def profile(user: User):
3047
"""
3148
return {'id': user.id,
3249
'username': user.username,
50+
'nick_name': user.nick_name,
3351
'email': user.email,
3452
'role': user.role,
3553
'permissions': [str(p) for p in []],
3654
'is_edit_password': user.password == 'd880e722c47a34d8e9fce789fc62389d' if user.role == 'ADMIN' else False,
3755
'language': user.language}
56+
57+
58+
class UserManageSerializer(serializers.Serializer):
59+
class UserInstance(serializers.Serializer):
60+
email = serializers.EmailField(
61+
required=True,
62+
label=_("Email"),
63+
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
64+
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
65+
66+
username = serializers.CharField(required=True,
67+
label=_("Username"),
68+
max_length=20,
69+
min_length=6,
70+
validators=[
71+
validators.RegexValidator(regex=re.compile("^.{6,20}$"),
72+
message=_(
73+
'Username must be 6-20 characters long'))
74+
])
75+
password = serializers.CharField(required=True, label=_("Password"), max_length=20, min_length=6,
76+
validators=[validators.RegexValidator(regex=re.compile(
77+
"^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~.()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)"
78+
"(?![0-9_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9_!@#$%^&*`~.()-+=]{6,20}$")
79+
, message=_(
80+
"The password must be 6-20 characters long and must be a combination of letters, numbers, and special characters."))])
81+
82+
nick_name = serializers.CharField(required=False, label=_("Nick name"), max_length=64,
83+
allow_null=True, allow_blank=True)
84+
phone = serializers.CharField(required=False, label=_("Phone"), max_length=20,
85+
allow_null=True, allow_blank=True)
86+
87+
def is_valid(self, *, raise_exception=True):
88+
super().is_valid(raise_exception=True)
89+
username = self.data.get('username')
90+
email = self.data.get('email')
91+
u = QuerySet(User).filter(Q(username=username) | Q(email=email)).first()
92+
if u is not None:
93+
if u.email == email:
94+
raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception()
95+
if u.username == username:
96+
raise ExceptionCodeConstants.USERNAME_IS_EXIST.value.to_app_api_exception()
97+
98+
@valid_license(model=User, count=2,
99+
message=_(
100+
'The community version supports up to 2 users. If you need more users, please contact us (https://fit2cloud.com/).'))
101+
@transaction.atomic
102+
def save(self, instance, with_valid=True):
103+
if with_valid:
104+
UserManageSerializer.UserInstance(data=instance).is_valid(raise_exception=True)
105+
106+
user = User(id=uuid.uuid7(), email=instance.get('email'),
107+
phone="" if instance.get('phone') is None else instance.get('phone'),
108+
nick_name="" if instance.get('nick_name') is None else instance.get('nick_name')
109+
, username=instance.get('username'), password=password_encrypt(instance.get('password')),
110+
role=RoleConstants.USER.name, source="LOCAL",
111+
is_active=True)
112+
user.save()
113+
return UserProfileSerializer(user).data

apps/users/urls.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,11 @@
99
path('user/captcha', views.CaptchaView.as_view(), name='captcha'),
1010
path('user/test', views.TestPermissionsUserView.as_view(), name="test"),
1111
path('workspace/<str:workspace_id>/user/profile', views.TestWorkspacePermissionUserView.as_view(),
12-
name="test_workspace_id_permission")
12+
name="test_workspace_id_permission"),
13+
path("user_manage", views.UserManage.as_view(), name="user_manage"),
14+
# path("user_manage/<str:user_id>", views.UserManage.Operate.as_view(), name="user_manage_operate"),
15+
# path("user_manage/<str:user_id>/re_password", views.UserManage.RePassword.as_view(),
16+
# name="user_manage_re_password"),
17+
# path("user_manage/<int:current_page>/<int:page_size>", views.UserManage.Page.as_view(),
18+
# name="user_manage_re_password"),
1319
]

apps/users/views/user.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from common.constants.permission_constants import PermissionConstants, Permission, Group, Operate
1717
from common.result import result
1818
from users.api.user import UserProfileAPI, TestWorkspacePermissionUserApi
19-
from users.serializers.user import UserProfileSerializer
19+
from users.serializers.user import UserProfileSerializer, UserManageSerializer
2020

2121

2222
class UserProfileView(APIView):
@@ -56,3 +56,17 @@ class TestWorkspacePermissionUserView(APIView):
5656
@has_permissions(PermissionConstants.USER_EDIT.get_workspace_permission())
5757
def get(self, request: Request, workspace_id):
5858
return result.success(UserProfileSerializer().profile(request.user))
59+
60+
61+
class UserManage(APIView):
62+
authentication_classes = [TokenAuth]
63+
64+
@extend_schema(methods=['POST'],
65+
description=_("Create user"),
66+
operation_id=_("Create user"),
67+
tags=[_("User management")],
68+
request=UserProfileAPI.get_request(),
69+
responses=UserProfileAPI.get_response())
70+
@has_permissions(PermissionConstants.USER_CREATE)
71+
def post(self, request: Request):
72+
return result.success(UserManageSerializer().save(request.data))

0 commit comments

Comments
 (0)