diff --git a/apps/chat/urls.py b/apps/chat/urls.py index 5fb3dc23fa0..5a386bda107 100644 --- a/apps/chat/urls.py +++ b/apps/chat/urls.py @@ -29,4 +29,8 @@ path('historical_conversation_record///', views.HistoricalConversationRecordView.PageView.as_view(), name='historical_conversation_record'), path('share/', ChatRecordDetailView.as_view()), path('/chat//share_chat', ChatRecordLinkView.as_view()), + path("chat_user/current/reset_password", views.ResetCurrentUserPasswordView.as_view(), name="reset_password_current"), + path('auth/login/', views.LocalLoginView.as_view()), + path('auth/logout', views.Logout.as_view(), name='logout'), + path('chat_user/profile', views.ChatUserProfileView.as_view(), name="chat_user_profile"), ] diff --git a/apps/chat/views/chat.py b/apps/chat/views/chat.py index cbb15fef085..8f1edfc10a6 100644 --- a/apps/chat/views/chat.py +++ b/apps/chat/views/chat.py @@ -6,7 +6,10 @@ @date:2025/6/6 11:18 @desc: """ +import json + import requests +from django.core.cache import cache from django.http import HttpResponse, StreamingHttpResponse from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema @@ -23,15 +26,24 @@ from chat.serializers.chat_authentication import AnonymousAuthenticationSerializer, ApplicationProfileSerializer, \ AuthProfileSerializer from common.auth import ChatTokenAuth +from common.auth.common import FileToken +from common.constants.authentication_type import AuthenticationType +from common.constants.cache_version import Cache_Version from common.constants.permission_constants import ChatAuth -from common.exception.app_exception import AppAuthenticationFailed -from common.log.log import _get_ip_address +from common.exception.app_exception import AppAuthenticationFailed, AppApiException +from common.log.log import _get_ip_address, log from common.result import result +from common.utils.rsa_util import decrypt from knowledge.models import FileSourceType from maxkb.const import CONFIG +from models_provider.api.model import DefaultModelResponse from oss.serializers.file import FileSerializer -from users.api import CaptchaAPI +from system_manage.serializers.chat_user import RePasswordSerializer, ChatUserProfileSerializer +from system_manage.serializers.chat_user_serializer import ChatUserAccessTokenSerializer +from users.api import CaptchaAPI, LoginAPI +from users.api.user import ResetPasswordAPI, UserProfileAPI from users.serializers.login import CaptchaSerializer +from users.views import get_re_password_details def stream_image(response): @@ -288,3 +300,120 @@ def post(self, request: Request, chat_id: str): request.auth.chat_user_id) file_ids.append({'name': file.name, 'url': file_url, 'file_id': file_url.split('/')[-1]}) return result.success(file_ids) + + +class ResetCurrentUserPasswordView(APIView): + authentication_classes = [ChatTokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Modify current user password"), + description=_("Modify current user password"), + operation_id=_("Modify current user password"), # type: ignore + tags=[_("Chat User")], # type: ignore + request=ResetPasswordAPI.get_request(), + responses=DefaultModelResponse.get_response(), + ) + @log( + menu="Chat User", + operate="Modify current user password", + get_operation_object=lambda r, k: {"name": r.user.username}, + get_details=get_re_password_details, + ) + def post(self, request: Request): + request_data = request.data + encrypted_data = request_data.get("encryptedData", "") + if encrypted_data: + try: + decrypted_raw = decrypt(encrypted_data) + # decrypt 可能返回非 JSON 字符串,防护解析异常 + decrypted_data = json.loads(decrypted_raw) if decrypted_raw else {} + if isinstance(decrypted_data, dict): + request_data = decrypted_data + except Exception as e: + raise AppApiException(500, _("Invalid encrypted data")) + serializer_obj = RePasswordSerializer(data=request_data) + if serializer_obj.reset_password(request.user.id): + version, get_key = Cache_Version.CHAT_USER_TOKEN.value + auth = request.META.get("HTTP_AUTHORIZATION") + cache.delete(get_key(token=auth), version=version) + return result.success(True) + return result.error(_("Failed to change password")) + + + +class ChatUserProfileView(APIView): + authentication_classes = [ChatTokenAuth] + + @extend_schema( + methods=["GET"], + summary=_("Get current user information"), + description=_("Get current user information"), + operation_id=_("Get current user information"), # type: ignore + tags=[_("Chat User")], # type: ignore + responses=UserProfileAPI.get_response(), + ) + def get(self, request: Request): + return result.success(ChatUserProfileSerializer().profile(request.user)) + + +class BaseAuthView(APIView): + @staticmethod + def create_token_and_cache(access_token, user, request): + token = ChatUserAccessTokenSerializer.create_token_and_cache(access_token, user, request) + version, get_key = Cache_Version.CHAT_USER_TOKEN.value + cache.set(get_key(token), user, timeout=60 * 60 * 2, version=version) + return token, FileToken(str(user.id), AuthenticationType.CHAT_USER.value).to_token() + + @classmethod + def generate(self, request, f_token: str, response: HttpResponse, path: str = '/chat'): + secure = request.is_secure() + response.set_cookie( + "mk_file_auth", + value=f_token, + max_age=7 * 24 * 3600, + path=path, + domain=None, + secure=secure, + httponly=True, + samesite="Lax", + ) + return response + + +class LocalLoginView(BaseAuthView): + @extend_schema( + methods=["POST"], + description=_("Log in"), + summary=_("Log in"), + operation_id=_("Log in"), # type: ignore + tags=[_("Chat User/login")], # type: ignore + request=LoginAPI.get_request(), + responses=LoginAPI.get_response(), + ) + def post(self, request: Request, access_token: str = None): + user = ChatUserAccessTokenSerializer.local_login(request.data, access_token) + user.source = "LOCAL" + token, f_token = self.create_token_and_cache(access_token, user, request) + response = result.success({'token': token}) + return self.generate(request, f_token, response, path=f'/chat/{access_token}/') + + +class Logout(APIView): + authentication_classes = [ChatTokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Sign out"), + description=_("Sign out"), + operation_id=_("Sign out"), # type: ignore + tags=[_("Chat User")], # type: ignore + responses=DefaultModelResponse.get_response(), + ) + @log(menu="Chat User/logout", operate="Sign out", get_operation_object=lambda r, k: {"name": r.user.username}) + def post(self, request: Request): + version, get_key = Cache_Version.CHAT_USER_TOKEN.value + auth = request.META.get("HTTP_AUTHORIZATION") + cache.delete(get_key(token=auth[7:]), version=version) + return result.success(True) + diff --git a/apps/system_manage/api/chat_user.py b/apps/system_manage/api/chat_user.py new file mode 100644 index 00000000000..3ff2e893ea9 --- /dev/null +++ b/apps/system_manage/api/chat_user.py @@ -0,0 +1,179 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: user.py + @date:2025/4/14 19:23 + @desc: +""" +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer +from users.serializers.user import CreateUserSerializer +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from system_manage.serializers.chat_user import ChatUserInstanceSerializer, ChatUserSerializer + + +class ChatUserResponse(ResultSerializer): + def get_data(self): + return ChatUserInstanceSerializer() + + +class CreateChatUserRequestSerializer(CreateUserSerializer): + user_group_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User Group IDs') + ) + + +class ChatUserAPI(APIMixin): + + @staticmethod + def get_response(): + return ChatUserResponse + + @staticmethod + def get_request(): + return CreateChatUserRequestSerializer + + @staticmethod + def get_parameters(): + return [OpenApiParameter( + name="user_id", + description=_('User ID'), + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + )] + + +class BatchAddGroupApi(APIMixin): + + @staticmethod + def get_request(): + return ChatUserSerializer.BatchAddGroup + + +class UserPasswordResponse(APIMixin): + + @staticmethod + def get_response(): + return PasswordResponse + + +class Password(serializers.Serializer): + password = serializers.CharField(required=True, label=_('Password')) + + +class PasswordResponse(ResultSerializer): + def get_data(self): + return Password() + + +class EditChatUserRequestSerializer(ChatUserSerializer.UserEditInstance): + user_group_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User Group IDs') + ) + + +class EditUserApi(APIMixin): + @staticmethod + def get_parameters(): + return [OpenApiParameter( + name="user_id", + description=_('User ID'), + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + )] + + @staticmethod + def get_request(): + return EditChatUserRequestSerializer + + +class ChatUserListResponse(serializers.Serializer): + id = serializers.CharField(required=True, label=_('ID')) + username = serializers.CharField(required=True, label=_('Username')) + nick_name = serializers.CharField(required=True, label=_('Nickname')) + email = serializers.EmailField(required=False, allow_blank=True, label=_('Email')) + phone = serializers.CharField(required=False, allow_blank=True, label=_('Phone')) + is_active = serializers.BooleanField(required=False, default=True, label=_('Is Active')) + user_group_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User Group IDs') + ) + user_group_names = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User Group Names') + ) + + +class ChatUsersListResponse(ResultSerializer): + def get_data(self): + return ChatUserListResponse(many=True) + + +class ChatUserPageApi(APIMixin): + @staticmethod + def get_parameters(): + return [OpenApiParameter( + name="username", + description=_('Username'), + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + ), + OpenApiParameter( + name="nick_name", + description=_('Nickname'), + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + ), + OpenApiParameter( + name="source", + description=_('Source'), + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + ), + OpenApiParameter( + name="is_active", + description=_('Is Active'), + type=OpenApiTypes.BOOL, + location=OpenApiParameter.QUERY, + required=False, + ), + OpenApiParameter( + name='current_page', + type=OpenApiTypes.INT, + description=_('Current page'), + required=True, + location=OpenApiParameter.PATH, + ), + OpenApiParameter( + name='page_size', + type=OpenApiTypes.INT, + description=_('Page size'), + required=True, + location=OpenApiParameter.PATH, + ), + + ] + + @staticmethod + def get_response(): + return ChatUsersListResponse + + + diff --git a/apps/system_manage/api/user_group.py b/apps/system_manage/api/user_group.py new file mode 100644 index 00000000000..9203f7fb8e0 --- /dev/null +++ b/apps/system_manage/api/user_group.py @@ -0,0 +1,128 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter +from rest_framework import serializers + +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer, DefaultResultSerializer +from system_manage.serializers.chat_user import UserGroupCreateSerializer, UserGroupModelSerializer +from django.utils.translation import gettext_lazy as _ + + +class UserGroupResponse(ResultSerializer): + def get_data(self): + return UserGroupModelSerializer() + + +class CreateUserGroupApi(APIMixin): + @staticmethod + def get_request(): + return UserGroupCreateSerializer + + @staticmethod + def get_response(): + return UserGroupResponse + + +class DeleteUserGroupApi(APIMixin): + @staticmethod + def get_parameters(): + return [OpenApiParameter( + name="user_group_id", + description=_("User Group ID"), + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + )] + + @staticmethod + def get_response(): + return DefaultResultSerializer() + + +class UserGroupListResponse(ResultSerializer): + def get_data(self): + return UserGroupModelSerializer(many=True) + + +class UserGroupListApi(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name='user_group_id', + type=OpenApiTypes.STR, + description=_('Group ID'), + required=True, + location=OpenApiParameter.PATH, + ), + OpenApiParameter( + name='current_page', + type=OpenApiTypes.INT, + description=_('Current page'), + required=True, + location=OpenApiParameter.PATH, + ), + OpenApiParameter( + name='page_size', + type=OpenApiTypes.INT, + description=_('Page size'), + required=True, + location=OpenApiParameter.PATH, + ), + OpenApiParameter( + name='username', + type=OpenApiTypes.STR, + description=_('Username'), + required=False, + location=OpenApiParameter.QUERY, + ), + OpenApiParameter( + name='nick_name', + type=OpenApiTypes.STR, + description=_('Nickname'), + required=False, + location=OpenApiParameter.QUERY, + ), + ] + + @staticmethod + def get_response(): + return UserGroupListResponse + + +class AddMemberRequest(serializers.Serializer): + user_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User IDs') + ) + + +class AddMemberApi(APIMixin): + @staticmethod + def get_parameters(): + return [OpenApiParameter( + name="user_group_id", + description=_("User Group ID"), + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + )] + + @staticmethod + def get_request(): + return AddMemberRequest + + +class RemoveMemberRequest(serializers.Serializer): + group_relation_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User group relation IDs') + ) + + +class RemoveMemberApi(APIMixin): + @staticmethod + def get_request(): + return RemoveMemberRequest diff --git a/apps/system_manage/serializers/chat_user.py b/apps/system_manage/serializers/chat_user.py new file mode 100644 index 00000000000..f512fb27218 --- /dev/null +++ b/apps/system_manage/serializers/chat_user.py @@ -0,0 +1,703 @@ +# coding=utf-8 +import json +import re +from collections import defaultdict + +import uuid_utils.compat as uuid +from django.core import validators +from django.db import transaction +from django.db.models import Q, QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.constants.exception_code_constants import ExceptionCodeConstants +from common.db.search import page_search +from common.exception.app_exception import AppApiException +from common.utils.common import password_encrypt +from common.utils.rsa_util import decrypt +from system_manage.models import ChatUser, UserGroup, UserGroupRelation +from users.serializers.user import PASSWORD_REGEX + + +class ChatUserInstanceSerializer(serializers.ModelSerializer): + class Meta: + model = ChatUser + fields = ['id', 'username', 'email', 'phone', 'is_active', 'nick_name', 'create_time', 'update_time', + 'source'] + + +@transaction.atomic +def add_or_edit_user_group_relation(user, user_group_ids): + UserGroupRelation.objects.filter(user=user).delete() + if not user_group_ids: + return + groups = UserGroup.objects.filter(id__in=user_group_ids) + if groups.count() != len(user_group_ids): + raise AppApiException(500, _('Some user groups do not exist')) + + UserGroupRelation.objects.bulk_create([ + UserGroupRelation(user=user, group=group) + for group in groups + ]) + + +class ChatUserSerializer(serializers.Serializer): + class UserInstance(serializers.Serializer): + email = serializers.EmailField( + required=False, + label=_("Email"), + validators=[validators.EmailValidator( + message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message, + code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code + )], + allow_null=True, + allow_blank=True + ) + username = serializers.CharField( + required=True, + label=_("Username"), + max_length=64, + min_length=4, + validators=[ + validators.RegexValidator( + regex=re.compile("^.{4,64}$"), + message=_('Username must be 4-64 characters long') + ) + ] + ) + password = serializers.CharField( + required=True, + label=_("Password"), + max_length=20, + min_length=6, + validators=[ + validators.RegexValidator( + regex=PASSWORD_REGEX, + 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=True, + label=_("Nick name"), + max_length=64, + ) + phone = serializers.CharField( + required=False, + label=_("Phone"), + max_length=20, + allow_null=True, + allow_blank=True + ) + user_group_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=False, + label=_('User Group IDs') + ) + source = serializers.CharField( + required=False, + label=_("Source"), + max_length=20, + default="LOCAL" + ) + + def is_valid(self, *, raise_exception=True): + super().is_valid(raise_exception=True) + self._check_unique_username_and_email() + + def _check_unique_username_and_email(self): + username = self.data.get('username') + nick_name = self.data.get('nick_name') + user = ChatUser.objects.filter(Q(username=username) | Q(nick_name=nick_name)).first() + if user: + if user.username == username: + raise ExceptionCodeConstants.USERNAME_IS_EXIST.value.to_app_api_exception() + if user.nick_name == nick_name: + raise ExceptionCodeConstants.NICKNAME_IS_EXIST.value.to_app_api_exception() + + class Query(serializers.Serializer): + username = serializers.CharField( + required=False, + label=_('Username'), + allow_null=True, + allow_blank=True + ) + nick_name = serializers.CharField( + required=False, + label=_('Nickname'), + allow_null=True, + allow_blank=True + ) + source = serializers.CharField( + required=False, + label=_('Source'), + allow_null=True, + allow_blank=True + ) + is_active = serializers.BooleanField( + required=False, + label=_("Is active"), + allow_null=True + ) + + def get_query_set(self): + username = self.data.get('username') + query_set = QuerySet(ChatUser) + if username is not None: + query_set = query_set.filter( + Q(username__contains=username)) + nick_name = self.data.get('nick_name') + if nick_name is not None: + query_set = query_set.filter( + Q(nick_name__contains=nick_name)) + source = self.data.get('source') + if source is not None: + query_set = query_set.filter(source=source) + is_active = self.data.get('is_active', None) + if is_active is not None: + query_set = query_set.filter(is_active=is_active) + query_set = query_set.order_by("-create_time") + return query_set + + def page(self, current_page: int, page_size: int, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + result = page_search(current_page, page_size, + self.get_query_set(), + post_records_handler=lambda u: ChatUserInstanceSerializer(u).data) + user_ids = [user['id'] for user in result['records']] + user_groups = UserGroupRelation.objects.filter( + user__id__in=user_ids + ).select_related('group') + + user_groups_map = defaultdict(lambda: {'user_group_ids': [], 'user_group_names': []}) + + for relation in user_groups: + user_groups_map[str(relation.user_id)]['user_group_ids'].append(str(relation.group_id)) + user_groups_map[str(relation.user_id)]['user_group_names'].append(relation.group.name) + + for user in result['records']: + user.update(user_groups_map.get(str(user['id']), {'user_group_ids': [], 'user_group_names': []})) + + return result + + class BatchDeleteInstance(serializers.Serializer): + ids = serializers.ListField( + child=serializers.UUIDField(required=True), + required=True, + label=_('User IDs') + ) + + def batch_delete(self): + user_ids = self.data.get('ids') + if not user_ids: + raise AppApiException(1004, _('User IDs cannot be empty')) + ChatUser.objects.filter(id__in=user_ids).delete() + return True + + class BatchAddGroup(serializers.Serializer): + ids = serializers.ListField( + child=serializers.UUIDField(required=True), + required=True, + label=_('User IDs') + ) + user_group_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User Group IDs') + ) + is_append = serializers.BooleanField( + required=False, + label=_('Is Append'), + default=False + ) + + @transaction.atomic + def batch_add_group(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + user_ids = self.data.get('ids') + original_group_ids = self.data.get('user_group_ids') + is_append = self.data.get('is_append', False) + + if not user_ids: + raise AppApiException(1004, _('User IDs cannot be empty')) + if not original_group_ids: + raise AppApiException(1004, _('User Group IDs cannot be empty')) + + users = ChatUser.objects.filter(id__in=user_ids) + if users.count() != len(user_ids): + raise AppApiException(1004, _('Some users do not exist')) + + groups_count = UserGroup.objects.filter(id__in=original_group_ids).count() + if groups_count != len(original_group_ids): + raise AppApiException(1004, _('Some user groups do not exist')) + + if is_append: + # 获取现有关系 + existing_relations = UserGroupRelation.objects.filter( + user_id__in=user_ids + ).values_list('user_id', 'group_id') + + existing_groups_map = defaultdict(set) + for user_id, group_id in existing_relations: + existing_groups_map[str(user_id)].add(group_id) + + # 准备要创建的新关系 + relations_to_create = [] + for user_id in user_ids: + # 只添加不在现有关系中的组 + new_group_ids = set(original_group_ids) - existing_groups_map.get(user_id, set()) + + for group_id in new_group_ids: + relations_to_create.append( + UserGroupRelation( + id=uuid.uuid7(), + user_id=user_id, + group_id=group_id + ) + ) + + # 只创建不存在的关系,不删除现有关系 + if relations_to_create: + UserGroupRelation.objects.bulk_create(relations_to_create, batch_size=1000) + + else: + # 非追加模式:直接批量删除旧关系,批量创建新关系 + UserGroupRelation.objects.filter(user_id__in=user_ids).delete() + + relations_to_create = [ + UserGroupRelation( + id=uuid.uuid7(), + user_id=user_id, + group_id=group_id + ) + for user_id in user_ids + for group_id in original_group_ids + ] + + if relations_to_create: + UserGroupRelation.objects.bulk_create(relations_to_create, batch_size=1000) + + @transaction.atomic + def save(self, instance, with_valid=True): + if with_valid: + if instance.get('encrypted'): + instance['password'] = decrypt(instance.get('password')) + self.UserInstance(data=instance).is_valid(raise_exception=True) + + user = ChatUser( + id=uuid.uuid7(), + email=instance.get('email'), + phone=instance.get('phone', ''), + nick_name=instance.get('nick_name', ''), + username=instance.get('username'), + password=password_encrypt(instance.get('password')), + source=instance.get('source', 'LOCAL'), + is_active=True + ) + user.save() + add_or_edit_user_group_relation(user, instance.get('user_group_ids', [])) + return ChatUserInstanceSerializer(user).data + + class UserEditInstance(serializers.Serializer): + email = serializers.EmailField( + required=False, + label=_("Email"), + validators=[validators.EmailValidator( + message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message, + code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code + )], + allow_null=True, + allow_blank=True + ) + nick_name = serializers.CharField( + required=True, + label=_("Name"), + max_length=64, + ) + phone = serializers.CharField( + required=False, + label=_("Phone"), + max_length=20, + allow_null=True, + allow_blank=True + ) + is_active = serializers.BooleanField( + required=False, + label=_("Is Active") + ) + user_group_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=False, + label=_('User Group IDs') + ) + + def is_valid(self, *, user_id=None, raise_exception=False): + super().is_valid(raise_exception=True) + self._check_unique_nick_name(user_id) + + def _check_unique_nick_name(self, user_id): + nick_name = self.data.get('nick_name') + if nick_name and ChatUser.objects.filter(nick_name=nick_name).exclude(id=user_id).exists(): + raise AppApiException(1008, _('Nickname is already in use')) + + class RePasswordInstance(serializers.Serializer): + password = serializers.CharField( + required=True, + label=_("Password"), + max_length=20, + min_length=6, + validators=[ + validators.RegexValidator( + regex=PASSWORD_REGEX, + message=_( + "The password must be 6-20 characters long and must be a combination of letters, numbers, and special characters." + ) + ) + ] + ) + re_password = serializers.CharField( + required=True, + label=_("Re Password"), + validators=[ + validators.RegexValidator( + regex=PASSWORD_REGEX, + message=_( + "The confirmation password must be 6-20 characters long and must be a combination of letters, numbers, and special characters." + ) + ) + ] + ) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + self._check_passwords_match() + + def _check_passwords_match(self): + if self.data.get('password') != self.data.get('re_password'): + raise ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.to_app_api_exception() + + class Operate(serializers.Serializer): + id = serializers.UUIDField(required=True, label=_('User ID')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + self._check_user_exists() + + def _check_user_exists(self): + if not ChatUser.objects.filter(id=self.data.get('id')).exists(): + raise AppApiException(1004, _('User does not exist')) + + @transaction.atomic + def delete(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + user_id = self.data.get('id') + ChatUser.objects.filter(id=user_id).delete() + return True + + def edit(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + ChatUserSerializer.UserEditInstance(data=instance).is_valid(user_id=self.data.get('id'), + raise_exception=True) + user = ChatUser.objects.filter(id=self.data.get('id')).first() + self._update_user_fields(user, instance) + user.save() + add_or_edit_user_group_relation(user, instance.get('user_group_ids', [])) + return ChatUserInstanceSerializer(user).data + + @staticmethod + def _update_user_fields(user, instance): + update_keys = ['email', 'nick_name', 'phone', 'is_active'] + for key in update_keys: + if key in instance and instance.get(key) is not None: + setattr(user, key, instance.get(key)) + + def one(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + user = ChatUser.objects.filter(id=self.data.get('id')).first() + user_data = ChatUserInstanceSerializer(user).data + # 补充用户组信息 + user_data['user_group_ids'] = list( + UserGroupRelation.objects.filter(user=user).values_list('group_id', flat=True)) + return user_data + + def re_password(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + encrypted_data = instance.get("encryptedData", "") + if encrypted_data: + try: + decrypted_raw = decrypt(encrypted_data) + # decrypt 可能返回非 JSON 字符串,防护解析异常 + decrypted_data = json.loads(decrypted_raw) if decrypted_raw else {} + if isinstance(decrypted_data, dict): + instance.update(decrypted_data) + except Exception as e: + raise AppApiException(500, _("Invalid encrypted data")) + ChatUserSerializer.RePasswordInstance(data=instance).is_valid(raise_exception=True) + user = ChatUser.objects.filter(id=self.data.get('id')).first() + user.password = password_encrypt(instance.get('password')) + user.save() + return True + + class GetUserListByGroup(serializers.Serializer): + group_id = serializers.UUIDField(required=True, label=_('Group ID')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + group_id = self.data.get('group_id') + if not UserGroup.objects.filter(id=group_id).exists(): + raise AppApiException(1004, _('User group does not exist')) + + def get_user_list(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + group_id = self.data.get('group_id') + user_ids = UserGroupRelation.objects.filter(group_id=group_id).values_list('user_id', flat=True) + users = ChatUser.objects.exclude(id__in=user_ids) + return ChatUserInstanceSerializer(users, many=True).data + + @classmethod + def list(cls): + users = ChatUser.objects.all().order_by('-create_time') + return ChatUserInstanceSerializer(users, many=True).data + + +class UserGroupModelSerializer(serializers.ModelSerializer): + class Meta: + model = UserGroup + fields = ['id', 'name'] + + +class UserGroupCreateSerializer(serializers.Serializer): + id = serializers.CharField(required=False, label='ID') + name = serializers.CharField(required=True, label='User Group Name') + + def validate(self, data): + id = data.get('id') + name = data.get('name') + if id: + group = UserGroup.objects.filter(id=id).first() + if not group: + raise AppApiException(500, _("User group does not exist")) + if name: + queryset = UserGroup.objects.filter(name=name) + if id: + # 排除当前用户组自身 + queryset = queryset.exclude(id=id) + if queryset.exists(): + raise AppApiException(500, _("User group name already exists")) + return data + + def create_or_update_group(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + id = self.data.get('id') + name = self.data.get('name') + + if id: + group = UserGroup.objects.get(id=id) + group.name = name + group.save() + else: + group = UserGroup.objects.create( + id=uuid.uuid7(), + name=name + ) + group.save() + return UserGroupModelSerializer(group).data + + def get_user_group_list(self): + groups = UserGroup.objects.all().order_by('name') + return UserGroupModelSerializer(groups, many=True).data + + class UserGroupDeleteSerializer(serializers.Serializer): + id = serializers.CharField(required=True, label='ID') + + def validate(self, data): + id = data.get('id') + group = UserGroup.objects.filter(id=id).first() + if not group: + raise AppApiException(500, _("User group does not exist")) + if group.id == 'default': + raise AppApiException(500, _("Default user group cannot be deleted")) + return data + + def delete(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + id = self.data.get('id') + UserGroupRelation.objects.filter(group_id=id).delete() + UserGroup.objects.filter(id=id).delete() + return True + + +class UserGroupAddMemberSerializer(serializers.Serializer): + id = serializers.CharField(required=True, label='ID') + user_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User IDs') + ) + + def validate(self, data): + id = data.get('id') + user_ids = data.get('user_ids') + group = UserGroup.objects.filter(id=id).first() + if not group: + raise AppApiException(500, _("User group does not exist")) + if not user_ids: + raise AppApiException(500, _("User IDs cannot be empty")) + return data + + def add_member(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + user_ids = self.data.get('user_ids') + current_user_group_ids = set( + str(user_id) for user_id in + UserGroupRelation.objects.filter(group__id=self.data.get('id')).values_list('user_id', flat=True) + ) + to_add = set(user_ids).difference(current_user_group_ids) + if to_add: + UserGroupRelation.objects.bulk_create([ + UserGroupRelation( + id=uuid.uuid7(), + user_id=user_id, + group_id=self.data.get('id') + ) + for user_id in to_add + ]) + return True + + +class UserGroupRemoveMemberSerializer(serializers.Serializer): + id = serializers.CharField(required=True, label='ID') + group_relation_ids = serializers.ListField( + child=serializers.CharField(required=True), + required=True, + label=_('User group relation IDs') + ) + + def validate(self, data): + id = data.get('id') + user_ids = data.get('group_relation_ids') + if UserGroup.objects.filter(id=id).count() == 0: + raise AppApiException(500, _("User group does not exist")) + if not user_ids: + raise AppApiException(500, _("User group relation IDs cannot be empty")) + return data + + def remove_member(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + group_relation_ids = self.data.get('group_relation_ids') + UserGroupRelation.objects.filter(id__in=group_relation_ids).delete() + return True + + +class UserGroupListPageSerializer(serializers.Serializer): + class Query(serializers.Serializer): + group_id = serializers.CharField(required=True, label=_('Group ID')) + username = serializers.CharField(required=False, label=_('Username'), allow_null=True) + nick_name = serializers.CharField(required=False, label=_('Nick Name'), allow_null=True) + source = serializers.CharField(required=False, label=_('Source'), allow_null=True) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=raise_exception) + group_id = self.data.get('group_id') + if not UserGroup.objects.filter(id=group_id).exists(): + raise AppApiException(500, _("User group does not exist")) + + def page(self, current_page, page_size): + self.is_valid() + query_set = self.get_query_set() + result = page_search( + current_page, + page_size, + query_set, + post_records_handler=lambda relation: { + **ChatUserInstanceSerializer(relation.user).data, + 'user_group_relation_id': relation.id + } + ) + return result + + def get_query_set(self): + group_id = self.data.get('group_id') + + username = self.data.get('username') + nick_name = self.data.get('nick_name') + source = self.data.get('source') + query_set = UserGroupRelation.objects.filter(group_id=group_id).select_related('user') + + if username is not None: + query_set = query_set.filter(user__username__contains=username) + if nick_name is not None: + query_set = query_set.filter(user__nick_name__contains=nick_name) + if source is not None: + query_set = query_set.filter(user__source=source) + return query_set.order_by('-user__create_time') + + +class RePasswordSerializer(serializers.Serializer): + password = serializers.CharField(required=True, label=_("Password"), + 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 confirmation password must be 6-20 characters long and must be a combination of letters, numbers, and special characters."))]) + + re_password = serializers.CharField(required=True, label=_("Confirm Password"), + 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 confirmation password must be 6-20 characters long and must be a combination of letters, numbers, and special characters."))] + ) + + class Meta: + model = ChatUser + fields = '__all__' + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + if self.data.get('password') != self.data.get('re_password'): + raise AppApiException(ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.code, + ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.message) + return True + + def reset_password(self, user_id): + """ + 修改密码 + :return: 是否成功 + """ + if self.is_valid(): + QuerySet(ChatUser).filter(id=user_id).update( + password=password_encrypt(self.data.get('password'))) + return True + + +class ChatUserProfileSerializer(serializers.Serializer): + @staticmethod + def profile(user: ChatUser): + """ + 获取对话用户详情 + @param user: 用户对象 + @return: + """ + if not user: + return {} + return { + 'id': user.id, + 'username': user.username, + 'nick_name': user.nick_name, + 'email': user.email, + 'source': user.source, + } diff --git a/apps/system_manage/serializers/chat_user_serializer.py b/apps/system_manage/serializers/chat_user_serializer.py new file mode 100644 index 00000000000..7ee0897db70 --- /dev/null +++ b/apps/system_manage/serializers/chat_user_serializer.py @@ -0,0 +1,154 @@ +import json + +from django.core.cache import cache +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from application.models import ApplicationAccessToken, ChatUserType +from common.auth.common import ChatUserToken, ChatAuthentication +from common.constants.authentication_type import AuthenticationType +from common.constants.cache_version import Cache_Version +from common.exception.app_exception import AppApiException +from common.log.log import record_log +from common.utils.common import password_encrypt +from common.utils.common import password_verify, needs_password_upgrade +from common.utils.rsa_util import decrypt +from system_manage.models import ResourceChatUserGroupAuthorize, ResourceType, \ + UserGroupRelation, ResourceChatUserAuthorize, ChatUser +from users.serializers.login import LoginRequest + +system_version, system_get_key = Cache_Version.SYSTEM.value + + +class ChatUserAccessTokenSerializer(serializers.Serializer): + + @staticmethod + def create_token_and_cache(access_token, user, request): + status = 500 # 默认失败状态 + workspace_id = 'default' + try: + application_access_token = ApplicationAccessToken.objects.filter( + access_token=access_token + ).first() + + if not application_access_token: + raise AppApiException(1005, _('Invalid access token')) + + application_id = application_access_token.application_id + workspace_id = application_access_token.application.workspace_id + # 检查用户是否有权限访问该应用 + is_authorized = ResourceChatUserAuthorize.objects.filter( + resource_id=application_id, + resource_type=ResourceType.APPLICATION.value, + is_auth=True, + user_id=user.id + ).exists() + if not is_authorized: + # 获取资源组授权的用户组ID + resource_group_ids = ResourceChatUserGroupAuthorize.objects.filter( + resource_id=application_id, + resource_type=ResourceType.APPLICATION.value, + is_auth=True, + ).values_list('user_group_id', flat=True) + + # 如果有资源组授权,则检查用户是否属于这些用户组 + if resource_group_ids.exists(): + is_authorized = UserGroupRelation.objects.filter( + user_id=user.id, + group_id__in=resource_group_ids + ).exists() + + if not is_authorized: + raise AppApiException(1005, _('The user does not have permission to access the application')) + token = ChatUserToken( + application_id, user.id, access_token, AuthenticationType.CHAT_USER, + ChatUserType.CHAT_USER, user.id, ChatAuthentication(user.source) + ).to_token() + status = 200 + return token + finally: + record_log( + menu='Chat User/login', + operate='Log in', + request=request, + user={'username': user.username}, + status=status, + operation_object={'name': user.username}, + workspace_id=workspace_id + ) + + @staticmethod + def get_auth_setting(access_token): + auth_setting = {} + application_access_token = ApplicationAccessToken.objects.filter( + access_token=access_token + ).first() + + if not application_access_token: + raise AppApiException(1005, _('Invalid access token')) + if application_access_token: + auth_setting = application_access_token.authentication_value + + return auth_setting + + @staticmethod + def local_login(instance, access_token): + username = instance.get("username", "") + encryptedData = instance.get("encryptedData", "") + if encryptedData: + json_data = json.loads(decrypt(encryptedData)) + instance.update(json_data) + try: + LoginRequest(data=instance).is_valid(raise_exception=True) + except Exception as e: + raise e + auth_setting = ChatUserAccessTokenSerializer.get_auth_setting(access_token) + + max_attempts = auth_setting.get("max_attempts", 1) + password = instance.get("password") + captcha = instance.get("captcha", "") + + # 判断是否需要验证码 + need_captcha = True + if max_attempts == -1: + need_captcha = False + elif max_attempts > 0: + fail_count = cache.get(system_get_key(f'chat_{username}'), version=system_version) or 0 + need_captcha = fail_count >= max_attempts + + if need_captcha: + if not captcha: + raise AppApiException(1005, _("Captcha is required")) + + captcha_cache = cache.get( + Cache_Version.CAPTCHA.get_key(captcha=f"chat_{username}"), + version=Cache_Version.CAPTCHA.get_version() + ) + if captcha_cache is None or captcha.lower() != captcha_cache: + raise AppApiException(1005, _("Captcha code error or expiration")) + + user = ChatUser.objects.filter(username=username).first() + + if not user or not password_verify(password, user.password): + record_login_fail(username) + raise AppApiException(500, _('The username or password is incorrect')) + + if needs_password_upgrade(user.password): + user.password = password_encrypt(password) + user.save(update_fields=['password']) + if not user.is_active: + raise AppApiException(1005, _("The user has been disabled, please contact the administrator!")) + cache.delete(system_get_key(f'chat_{username}'), version=system_version) + return user + + +def record_login_fail(username: str, expire: int = 600): + """记录登录失败次数""" + if not username: + return + fail_key = system_get_key(f'chat_{username}') + fail_count = cache.get(fail_key, version=system_version) + if fail_count is None: + cache.set(fail_key, 1, timeout=expire, version=system_version) + else: + cache.incr(fail_key, 1, version=system_version) diff --git a/apps/system_manage/urls.py b/apps/system_manage/urls.py index 078da32f922..6685bcc3ab7 100644 --- a/apps/system_manage/urls.py +++ b/apps/system_manage/urls.py @@ -14,5 +14,17 @@ path('workspace//mapping_resource////', views.MappingResourceView.as_view()), path('email_setting', views.SystemSetting.Email.as_view()), path('profile', views.SystemProfile.as_view()), - path('valid//', views.Valid.as_view()) + path('system/chat_user', views.SystemChatUserView.as_view()), + path('system/chat_user/list', views.SystemChatUserView.List.as_view()), + path('system/chat_user/batch_delete', views.SystemChatUserView.BatchDelete.as_view()), + path("system/chat_user/batch_add_group", views.SystemChatUserView.BatchAddGroup.as_view()), + path("system/chat_user/", views.SystemChatUserView.Operate.as_view()), + path("system/chat_user//re_password", views.SystemChatUserView.RePassword.as_view()), + path("system/chat_user/user_manage//", views.SystemChatUserView.Page.as_view()), + path('system/chat_user/group/', views.SystemChatUserView.GetUserListByGroup.as_view()), + path('system/group', views.SystemChatUserGroupView.as_view()), + path('system/group/', views.SystemChatUserGroupView.Delete.as_view()), + path('system/group//add_member', views.SystemChatUserGroupView.AddMember.as_view()), + path('system/group//remove_member', views.SystemChatUserGroupView.RemoveMember.as_view()), + path('system/group//user_list//', views.SystemChatUserGroupView.UserList.as_view()), ] diff --git a/apps/system_manage/views/__init__.py b/apps/system_manage/views/__init__.py index b8af19c4b5e..07d2ac17d65 100644 --- a/apps/system_manage/views/__init__.py +++ b/apps/system_manage/views/__init__.py @@ -11,3 +11,4 @@ from .system_profile import * from .valid import * from .resource_mapping import * +from .system_chat_user import * diff --git a/apps/system_manage/views/system_chat_user.py b/apps/system_manage/views/system_chat_user.py new file mode 100644 index 00000000000..c03ad64cdb4 --- /dev/null +++ b/apps/system_manage/views/system_chat_user.py @@ -0,0 +1,381 @@ +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from common.auth import TokenAuth +from common.auth.authentication import has_permissions +from common.constants.permission_constants import PermissionConstants, RoleConstants +from common.log.log import log +from common.result import result +from models_provider.api.model import DefaultModelResponse +from system_manage.api.chat_user import BatchAddGroupApi, ChatUserAPI, ChatUserPageApi, EditUserApi +from system_manage.api.user_group import AddMemberApi, CreateUserGroupApi, DeleteUserGroupApi, RemoveMemberApi, \ + UserGroupListApi +from system_manage.models import ChatUser, UserGroup +from system_manage.serializers.chat_user import ( + ChatUserSerializer, + UserGroupAddMemberSerializer, + UserGroupCreateSerializer, + UserGroupListPageSerializer, + UserGroupRemoveMemberSerializer, +) +from users.api.user import ChangeUserPasswordApi, DeleteUserApi, UserPageApi, UserProfileAPI + + +def get_user_operation_object(user_id): + user_model = QuerySet(model=ChatUser).filter(id=user_id).first() + if user_model is not None: + return {"name": user_model.username} + return {} + + +def get_batch_delete_user_operation_object(user_ids): + user_models = QuerySet(model=ChatUser).filter(id__in=user_ids) + if user_models.exists(): + return {"name": ", ".join([user.username for user in user_models])} + return {} + + +def get_user_group_operation_object(user_group_id): + user_group_model = QuerySet(model=UserGroup).filter(id=user_group_id).first() + if user_group_model is not None: + return {"name": user_group_model.name} + return {} + + +class SystemChatUserView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Create chat user"), + description=_("Create chat user"), + operation_id=_("Create chat user"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + request=ChatUserAPI.get_request(), + responses=ChatUserAPI.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_CREATE, RoleConstants.ADMIN) + @log( + menu="User management", + operate="Add user", + get_operation_object=lambda r, k: {"name": r.data.get("username", None)}, + ) + def post(self, request: Request): + return result.success(ChatUserSerializer().save(request.data)) + + class List(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + summary=_("Get chat user list"), + description=_("Get chat user list"), + operation_id=_("Get chat user list"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + responses=ChatUserPageApi.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_READ, PermissionConstants.USER_GROUP_READ, RoleConstants.ADMIN) + def get(self, request: Request): + return result.success(ChatUserSerializer.list()) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["DELETE"], + description=_("Delete chat user"), + summary=_("Delete chat user"), + operation_id=_("Delete chat user"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + parameters=DeleteUserApi.get_parameters(), + responses=DefaultModelResponse.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_DELETE, RoleConstants.ADMIN) + @log( + menu="User management", + operate="Delete user", + get_operation_object=lambda r, k: get_user_operation_object(k.get("user_id")), + ) + def delete(self, request: Request, user_id): + return result.success(ChatUserSerializer.Operate(data={"id": user_id}).delete(with_valid=True)) + + @extend_schema( + methods=["GET"], + summary=_("Get chat user information"), + description=_("Get chat user information"), + operation_id=_("Get chat user information"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + request=DeleteUserApi.get_parameters(), + responses=UserProfileAPI.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_READ, RoleConstants.ADMIN) + def get(self, request: Request, user_id): + return result.success(ChatUserSerializer.Operate(data={"id": user_id}).one(with_valid=True)) + + @extend_schema( + methods=["PUT"], + summary=_("Update chat user information"), + description=_("Update chat user information"), + operation_id=_("Update chat user information"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + parameters=DeleteUserApi.get_parameters(), + request=EditUserApi.get_request(), + responses=UserProfileAPI.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_EDIT, RoleConstants.ADMIN) + @log( + menu="Chat user", + operate="Update user information", + get_operation_object=lambda r, k: get_user_operation_object(k.get("user_id")), + ) + def put(self, request: Request, user_id): + return result.success(ChatUserSerializer.Operate(data={"id": user_id}).edit(request.data, with_valid=True)) + + class GetUserListByGroup(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + summary=_("Get user list by group"), + description=_("Get user list by group"), + operation_id=_("Get user list by group"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + request=AddMemberApi.get_parameters(), + responses=UserProfileAPI.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_READ, RoleConstants.ADMIN) + def get(self, request: Request, user_group_id): + return result.success( + ChatUserSerializer.GetUserListByGroup(data={"group_id": user_group_id}).get_user_list() + ) + + class BatchDelete(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Batch delete chat user"), + description=_("Batch delete chat user"), + operation_id=_("Batch delete chat user"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + request=DeleteUserApi.get_request(), + responses=DefaultModelResponse.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_DELETE, RoleConstants.ADMIN) + @log( + menu="Chat user", + operate="Batch delete user", + get_operation_object=lambda r, k: get_batch_delete_user_operation_object(r.data.get("ids", [])), + ) + def post(self, request: Request): + return result.success(ChatUserSerializer.BatchDeleteInstance({"ids": request.data}).batch_delete()) + + class BatchAddGroup(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Batch add chat user to group"), + description=_("Batch add chat user to group"), + operation_id=_("Batch add chat user to group"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + request=BatchAddGroupApi.get_request(), + responses=DefaultModelResponse.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_GROUP, RoleConstants.ADMIN) + @log( + menu="Chat user", + operate="Batch add user to group", + get_operation_object=lambda r, k: get_batch_delete_user_operation_object(r.data.get("user_group_ids", [])), + ) + def post(self, request: Request): + return result.success(ChatUserSerializer.BatchAddGroup(data=request.data).batch_add_group(with_valid=True)) + + class RePassword(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["PUT"], + summary=_("Change chat user password"), + description=_("Change chat user password"), + operation_id=_("Change chat user password"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + parameters=DeleteUserApi.get_parameters(), + request=ChangeUserPasswordApi.get_request(), + responses=DefaultModelResponse.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_EDIT, RoleConstants.ADMIN) + @log( + menu="Chat user", + operate="Change password", + get_operation_object=lambda r, k: get_user_operation_object(k.get("user_id")), + ) + def put(self, request: Request, user_id): + return result.success( + ChatUserSerializer.Operate(data={"id": user_id}).re_password(request.data, with_valid=True) + ) + + class Page(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + summary=_("Get user paginated list"), + description=_("Get user paginated list"), + operation_id=_("Get user paginated list"), # type: ignore + tags=[_("System/Chat user")], # type: ignore + parameters=ChatUserPageApi.get_parameters(), + responses=UserPageApi.get_response(), + ) + @has_permissions(PermissionConstants.CHAT_USER_READ, RoleConstants.ADMIN) + def get(self, request: Request, current_page, page_size): + d = ChatUserSerializer.Query( + data={ + "username": request.query_params.get("username", None), + "nick_name": request.query_params.get("nick_name", None), + "source": request.query_params.get("source", None), + "is_active": request.query_params.get("is_active", None), + "user_id": str(request.user.id), + } + ) + return result.success(d.page(current_page, page_size)) + + +class SystemChatUserGroupView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Create or update Chat User Group"), + description=_("Create or update Chat User Group"), + operation_id=_("Create or update Chat User Group"), # type: ignore + request=CreateUserGroupApi.get_request(), + responses=CreateUserGroupApi.get_response(), + tags=[_("System/User Group")], # type: ignore + ) # type: ignore + @has_permissions(PermissionConstants.USER_GROUP_CREATE, PermissionConstants.USER_GROUP_EDIT, RoleConstants.ADMIN) + @log( + menu="User group", + operate="Create or update user group", + get_operation_object=lambda r, k: {"name": r.data.get("name", None)}, + ) + def post(self, request: Request): + return result.success(UserGroupCreateSerializer(data=request.data).create_or_update_group(with_valid=True)) + + @extend_schema( + methods=["GET"], + summary=_("Get user group list"), + description=_("Get user group list"), + operation_id=_("Get user group list"), # type: ignore + responses=UserGroupListApi.get_response(), + tags=[_("System/User Group")], # type: ignore + ) + @has_permissions(PermissionConstants.USER_GROUP_READ, RoleConstants.ADMIN) + def get(self, request: Request): + return result.success(UserGroupCreateSerializer().get_user_group_list()) + + class Delete(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["DELETE"], + summary=_("Delete chat user group"), + description=_("Delete chat user group"), + operation_id=_("Delete chat user group"), # type: ignore + parameters=DeleteUserGroupApi.get_parameters(), + responses=DefaultModelResponse, + tags=[_("System/User Group")], # type: ignore + ) + @has_permissions(PermissionConstants.USER_GROUP_DELETE, RoleConstants.ADMIN) + @log( + menu="User group", + operate="Delete user group", + get_operation_object=lambda r, k: get_user_group_operation_object(k.get("user_group_id")), + ) + def delete(self, request: Request, user_group_id: str): + return result.success( + UserGroupCreateSerializer.UserGroupDeleteSerializer(data={"id": user_group_id}).delete() + ) + + class AddMember(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Add member to chat user group"), + description=_("Add member to chat user group"), + operation_id=_("Add member to chat user group"), # type: ignore + parameters=AddMemberApi.get_parameters(), + request=AddMemberApi.get_request(), + responses=DefaultModelResponse, + tags=[_("System/User Group")], # type: ignore + ) + @has_permissions(PermissionConstants.USER_GROUP_ADD_MEMBER, RoleConstants.ADMIN) + @log( + menu="User group", + operate="Add member to user group", + get_operation_object=lambda r, k: get_user_group_operation_object(k.get("user_group_id")), + get_user=lambda r: {"user_name": None, "email": None}, + get_details=lambda r: {"user_ids": r.data.get("user_ids", [])}, + ) + def post(self, request: Request, user_group_id: str): + return result.success( + UserGroupAddMemberSerializer( + data={"id": user_group_id, "user_ids": request.data.get("user_ids", [])} + ).add_member() + ) + + class RemoveMember(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["POST"], + summary=_("Remove member from chat user group"), + description=_("Remove member from chat user group"), + operation_id=_("Remove member from chat user group"), # type: ignore + parameters=AddMemberApi.get_parameters(), + request=RemoveMemberApi.get_request(), + responses=DefaultModelResponse, + tags=[_("System/User Group")], # type: ignore + ) + @has_permissions(PermissionConstants.USER_GROUP_REMOVE_MEMBER, RoleConstants.ADMIN) + @log( + menu="User group", + operate="Remove member from user group", + get_operation_object=lambda r, k: get_user_group_operation_object(k.get("user_group_id")), + get_user=lambda r: {"user_name": None, "email": None}, + get_details=lambda r: {"group_relation_ids": r.data.get("group_relation_ids", [])}, + ) + def post(self, request: Request, user_group_id: str): + return result.success( + UserGroupRemoveMemberSerializer( + data={"id": user_group_id, "group_relation_ids": request.data.get("group_relation_ids", [])} + ).remove_member() + ) + + class UserList(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + summary=_("Get user list by group"), + description=_("Get user list by group"), + operation_id=_("Get user list by group"), # type: ignore + tags=[_("System/User Group")], # type: ignore + parameters=UserGroupListApi.get_parameters(), + responses=UserGroupListApi.get_response(), + ) + @has_permissions(PermissionConstants.USER_GROUP_READ, RoleConstants.ADMIN) + def get(self, request: Request, user_group_id: str, current_page: int, page_size: int): + d = UserGroupListPageSerializer.Query( + data={ + "username": request.query_params.get("username", None), + "nick_name": request.query_params.get("nick_name", None), + "source": request.query_params.get("source", None), + "group_id": user_group_id, + } + ) + return result.success(d.page(current_page, page_size))