refactor: add logout

This commit is contained in:
wxg0103 2025-06-07 19:03:18 +08:00
parent b8b14884bd
commit 8d40004c67
9 changed files with 380 additions and 10 deletions

View File

@ -10,9 +10,9 @@ 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 common.result import ResultSerializer, DefaultResultSerializer
from users.serializers.user import UserProfileResponse, CreateUserSerializer, UserManageSerializer, \
UserInstanceSerializer
UserInstanceSerializer, RePasswordSerializer, CheckCodeSerializer, SendEmailSerializer
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@ -192,3 +192,29 @@ class TestWorkspacePermissionUserApi(APIMixin):
# 指定必须给
required=True,
)]
class ResetPasswordAPI(APIMixin):
@staticmethod
def get_request():
return RePasswordSerializer
class CheckCodeAPI(APIMixin):
@staticmethod
def get_request():
return CheckCodeSerializer
@staticmethod
def get_response():
return DefaultResultSerializer
class SendEmailAPI(APIMixin):
@staticmethod
def get_request():
return SendEmailSerializer
@staticmethod
def get_response():
return DefaultResultSerializer

View File

@ -6,10 +6,13 @@
@date2025/4/14 19:18
@desc:
"""
import datetime
import os
import random
import re
from collections import defaultdict
from itertools import product
from django.core.mail.backends.smtp import EmailBackend
from django.db import transaction
from django.db.models import Q, QuerySet
from rest_framework import serializers
@ -20,9 +23,13 @@ from common.database_model_manage.database_model_manage import DatabaseModelMana
from common.db.search import page_search
from common.exception.app_exception import AppApiException
from common.utils.common import valid_license, password_encrypt
from maxkb.conf import PROJECT_DIR
from system_manage.models import SystemSetting, SettingType
from users.models import User
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, to_locale
from django.core import validators
from django.core.mail import send_mail
from django.utils.translation import get_language
PASSWORD_REGEX = re.compile(
r"^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~.()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)"
@ -441,3 +448,169 @@ def update_user_role(instance, user):
workspace_id=workspace_id,
user_id=user.id
)
class RePasswordSerializer(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)])
code = serializers.CharField(required=True, label=_("Verification code"))
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 = User
fields = '__all__'
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
email = self.data.get("email")
# TODO 删除缓存
# cache_code = user_cache.get(email + ':reset_password')
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)
# if cache_code != self.data.get('code'):
# raise AppApiException(ExceptionCodeConstants.CODE_ERROR.value.code,
# ExceptionCodeConstants.CODE_ERROR.value.message)
return True
def reset_password(self):
"""
修改密码
:return: 是否成功
"""
if self.is_valid():
email = self.data.get("email")
QuerySet(User).filter(email=email).update(
password=password_encrypt(self.data.get('password')))
code_cache_key = email + ":reset_password"
# 删除验证码缓存
# user_cache.delete(code_cache_key)
return True
class SendEmailSerializer(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)])
type = serializers.CharField(required=True, label=_("Type"), validators=[
validators.RegexValidator(regex=re.compile("^register|reset_password$"),
message=_("The type only supports register|reset_password"), code=500)
])
class Meta:
model = User
fields = '__all__'
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=raise_exception)
user_exists = QuerySet(User).filter(email=self.data.get('email')).exists()
if not user_exists and self.data.get('type') == 'reset_password':
raise ExceptionCodeConstants.EMAIL_IS_NOT_EXIST.value.to_app_api_exception()
elif user_exists and self.data.get('type') == 'register':
raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception()
code_cache_key = self.data.get('email') + ":" + self.data.get("type")
code_cache_key_lock = code_cache_key + "_lock"
ttl = None # user_cache.ttl(code_cache_key_lock)
if ttl is not None:
raise AppApiException(500, _("Do not send emails again within {seconds} seconds").format(
seconds=int(ttl.total_seconds())))
return True
def send(self):
"""
发送邮件
:return: 是否发送成功
:exception 发送失败异常
"""
email = self.data.get("email")
state = self.data.get("type")
# 生成随机验证码
code = "".join(list(map(lambda i: random.choice(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'
]), range(6))))
# 获取邮件模板
language = get_language()
file = open(
os.path.join(PROJECT_DIR, "apps", "common", 'template', f'email_template_{to_locale(language)}.html'), "r",
encoding='utf-8')
content = file.read()
file.close()
code_cache_key = email + ":" + state
code_cache_key_lock = code_cache_key + "_lock"
# 设置缓存
# user_cache.set(code_cache_key_lock, code, timeout=datetime.timedelta(minutes=1))
system_setting = QuerySet(SystemSetting).filter(type=SettingType.EMAIL.value).first()
if system_setting is None:
# user_cache.delete(code_cache_key_lock)
raise AppApiException(1004,
_("The email service has not been set up. Please contact the administrator to set up the email service in [Email Settings]."))
try:
connection = EmailBackend(system_setting.meta.get("email_host"),
system_setting.meta.get('email_port'),
system_setting.meta.get('email_host_user'),
system_setting.meta.get('email_host_password'),
system_setting.meta.get('email_use_tls'),
False,
system_setting.meta.get('email_use_ssl')
)
# 发送邮件
send_mail(_('【Intelligent knowledge base question and answer system-{action}').format(
action=_('User registration') if state == 'register' else _('Change password')),
'',
html_message=f'{content.replace("${code}", code)}',
from_email=system_setting.meta.get('from_email'),
recipient_list=[email], fail_silently=False, connection=connection)
except Exception as e:
# user_cache.delete(code_cache_key_lock)
raise AppApiException(500, f"{str(e)}" + _("Email sending failed"))
# user_cache.set(code_cache_key, code, timeout=datetime.timedelta(minutes=30))
return True
class CheckCodeSerializer(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)])
code = serializers.CharField(required=True, label=_("Verification code"))
type = serializers.CharField(required=True,
label=_("Type"),
validators=[
validators.RegexValidator(regex=re.compile("^register|reset_password$"),
message=_(
"The type only supports register|reset_password"),
code=500)
])
def is_valid(self, *, raise_exception=False):
super().is_valid()
#TODO 这里的缓存 需要重新设计
value = None#user_cache.get(self.data.get("email") + ":" + self.data.get("type"))
if value is None or value != self.data.get("code"):
raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception()
return True

View File

@ -8,6 +8,11 @@ urlpatterns = [
path('user/profile', views.UserProfileView.as_view(), name="user_profile"),
path('user/captcha', views.CaptchaView.as_view(), name='captcha'),
path('user/test', views.TestPermissionsUserView.as_view(), name="test"),
path('user/logout', views.Logout.as_view(), name='logout'),
path("user/send_email", views.SendEmail.as_view(), name='send_email'),
path("user/check_code", views.CheckCode.as_view(), name='check_code'),
path("user/re_password", views.RePasswordView.as_view(), name='re_password'),
path("user/current/send_email", views.SendEmailToCurrentUserView.as_view(), name="send_email_current"),
path('workspace/<str:workspace_id>/user_list', views.WorkspaceUserListView.as_view(),
name="test_workspace_id_permission"),
path('workspace/<str:workspace_id>/user/profile', views.TestWorkspacePermissionUserView.as_view(),

View File

@ -6,14 +6,18 @@
@date2025/4/14 10:22
@desc:
"""
from django.core.cache import cache
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 import result
from common.auth import TokenAuth
from common.constants.cache_version import Cache_Version
from common.log.log import log
from common.utils.common import encryption
from models_provider.api.model import DefaultModelResponse
from users.api.login import LoginAPI, CaptchaAPI
from users.serializers.login import LoginSerializer, CaptchaSerializer
@ -44,6 +48,23 @@ class LoginView(APIView):
return result.success(LoginSerializer().login(request.data))
class Logout(APIView):
authentication_classes = [TokenAuth]
@extend_schema(methods=['POST'],
summary=_("Sign out"),
description=_("Sign out"),
operation_id=_("Sign out"), # type: ignore
tags=[_("User Management")], # type: ignore
responses=DefaultModelResponse.get_response())
@log(menu='User management', operate='Sign out',
get_operation_object=lambda r, k: {'name': r.user.username})
def post(self, request: Request):
version, get_key = Cache_Version.TOKEN.value
cache.delete(get_key(token=request.auth), version=version)
return result.success(True)
class CaptchaView(APIView):
@extend_schema(methods=['GET'],
summary=_("Get captcha"),

View File

@ -20,10 +20,12 @@ from common.result import result
from maxkb.const import CONFIG
from models_provider.api.model import DefaultModelResponse
from tools.serializers.tool import encryption
from users.api import SendEmailAPI, CheckCodeAPI, ResetPasswordAPI
from users.api.user import UserProfileAPI, TestWorkspacePermissionUserApi, DeleteUserApi, EditUserApi, \
ChangeUserPasswordApi, UserPageApi, UserListApi, UserPasswordResponse, WorkspaceUserAPI
from users.models import User
from users.serializers.user import UserProfileSerializer, UserManageSerializer
from users.serializers.user import UserProfileSerializer, UserManageSerializer, CheckCodeSerializer, \
SendEmailSerializer, RePasswordSerializer
default_password = CONFIG.get('default_password', 'MaxKB@123..')
@ -223,3 +225,73 @@ class UserManage(APIView):
data={'email_or_username': request.query_params.get('email_or_username', None),
'user_id': str(request.user.id)})
return result.success(d.page(current_page, page_size))
class RePasswordView(APIView):
@extend_schema(methods=['POST'],
summary=_("Change password"),
description=_("Change password"),
operation_id=_("Change password"), # type: ignore
tags=[_("User Management")], # type: ignore
request=ResetPasswordAPI.get_request(),
responses=DefaultModelResponse.get_response())
@log(menu='User management', operate='Change password',
get_operation_object=lambda r, k: {'name': r.data.get('email', None)},
get_user=lambda r: {'user_name': None, 'email': r.data.get('email', None)},
get_details=get_re_password_details)
def post(self, request: Request):
serializer_obj = RePasswordSerializer(data=request.data)
return result.success(serializer_obj.reset_password())
class SendEmail(APIView):
@extend_schema(methods=['POST'],
summary=_("Send email"),
description=_("Send email"),
operation_id=_("Send email"), # type: ignore
tags=[_("User Management")], # type: ignore
request=SendEmailAPI().get_request(),
responses=SendEmailAPI().get_response())
@log(menu='User management', operate='Send email',
get_operation_object=lambda r, k: {'name': r.data.get('email', None)},
get_user=lambda r: {'user_name': None, 'email': r.data.get('email', None)})
def post(self, request: Request):
serializer_obj = SendEmailSerializer(data=request.data)
if serializer_obj.is_valid(raise_exception=True):
return result.success(serializer_obj.send())
class CheckCode(APIView):
@extend_schema(methods=['POST'],
summary=_("Check whether the verification code is correct"),
description=_("Check whether the verification code is correct"),
operation_id=_("Check whether the verification code is correct"), # type: ignore
tags=[_("User Management")], # type: ignore
request=CheckCodeAPI().get_request(),
responses=CheckCodeAPI().get_response())
@log(menu='User management', operate='Check whether the verification code is correct',
get_operation_object=lambda r, k: {'name': r.data.get('email', None)},
get_user=lambda r: {'user_name': None, 'email': r.data.get('email', None)})
def post(self, request: Request):
return result.success(CheckCodeSerializer(data=request.data).is_valid(raise_exception=True))
class SendEmailToCurrentUserView(APIView):
authentication_classes = [TokenAuth]
@extend_schema(methods=['POST'],
summary=_("Send email to current user"),
description=_("Send email to current user"),
operation_id=_("Send email to current user"), # type: ignore
tags=[_("User Management")], # type: ignore
request=SendEmailAPI().get_request(),
responses=SendEmailAPI().get_response())
@log(menu='User management', operate='Send email to current user',
get_operation_object=lambda r, k: {'name': r.user.username})
def post(self, request: Request):
serializer_obj = SendEmailSerializer(data={'email': request.user.email, 'type': "reset_password"})
if serializer_obj.is_valid(raise_exception=True):
return result.success(serializer_obj.send())

View File

@ -0,0 +1,58 @@
import {Result} from '@/request/Result'
import {get, post, del, put} from '@/request/index'
import {type Ref} from 'vue'
const prefix = '/system/api_key'
/**
* API_KEY列表
*/
const getAPIKey: (loading?: Ref<boolean>) => Promise<Result<any>> = () => {
return get(`${prefix}`)
}
/**
* API_KEY
*/
const postAPIKey: (loading?: Ref<boolean>) => Promise<Result<any>> = (
loading
) => {
return post(`${prefix}`, {}, undefined, loading)
}
/**
* API_KEY
* @param application_id api_key_id
*/
const delAPIKey: (
api_key_id: string,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (api_key_id, loading) => {
return del(`${prefix}/${api_key_id}`, undefined, undefined, loading)
}
/**
* API_KEY
* data {
* is_active: boolean
* }
* @param api_key_id
* @param data
* @param loading
*/
const putAPIKey: (
api_key_id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (api_key_id, data, loading) => {
return put(`${prefix}/${api_key_id}`, data, undefined, loading)
}
export default {
getAPIKey,
postAPIKey,
delAPIKey,
putAPIKey
}

View File

@ -2,6 +2,7 @@ import { Result } from '@/request/Result'
import { get, put, post, del } from '@/request/index'
import type { pageRequest } from '@/api/type/common'
import type { Ref } from 'vue'
import type {ResetPasswordRequest} from "@/api/type/user.ts";
const prefix = '/user_manage'
/**
@ -64,11 +65,24 @@ const putUserManagePassword: (
return put(`${prefix}/${user_id}/re_password`, data, undefined, loading)
}
/**
*
* @param request
* @param loading
* @returns
*/
const resetPassword: (
request: ResetPasswordRequest,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (request, loading) => {
return post('/user/re_password', request, undefined, loading)
}
export default {
getUserManage,
putUserManage,
delUserManage,
postUserManage,
putUserManagePassword
putUserManagePassword,
resetPassword
}

View File

@ -87,9 +87,10 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<APIKeyDialog :user-id="user.userInfo?.id" ref="APIKeyDialogRef" />
<!-- <ResetPassword ref="resetPasswordRef"></ResetPassword> -->
<!-- <AboutDialog ref="AboutDialogRef"></AboutDialog>
<APIKeyDialog :user-id="user.userInfo?.id" ref="APIKeyDialogRef" /> -->
-->
<!-- <UserPwdDialog ref="UserPwdDialogRef" /> -->
</template>
<script setup lang="ts">
@ -99,14 +100,14 @@ import { useRouter } from 'vue-router'
// import ResetPassword from './ResetPassword.vue'
// import AboutDialog from './AboutDialog.vue'
// import UserPwdDialog from '@/views/user-manage/component/UserPwdDialog.vue'
// import APIKeyDialog from './APIKeyDialog.vue'
import APIKeyDialog from './APIKeyDialog.vue'
import { ComplexPermission } from '@/utils/permission/type'
import { langList } from '@/locales/index'
import { useLocale } from '@/locales/useLocale'
import type ResetPassword from "@/layout/layout-header/avatar/ResetPassword.vue";
const { user } = useStore()
const router = useRouter()
const UserPwdDialogRef = ref()
const AboutDialogRef = ref()
const APIKeyDialogRef = ref()
const resetPasswordRef = ref<InstanceType<typeof ResetPassword>>()

View File

@ -59,7 +59,7 @@ import type { ResetPasswordRequest } from '@/api/type/user'
import { useRouter, useRoute } from 'vue-router'
import { MsgSuccess } from '@/utils/message'
import type { FormInstance, FormRules } from 'element-plus'
import UserApi from '@/api/user'
import UserApi from '@/api/user/user-manage'
import { t } from '@/locales'
const router = useRouter()
const route = useRoute()