Skip to content

Commit 8d40004

Browse files
committed
refactor: add logout
1 parent b8b1488 commit 8d40004

File tree

9 files changed

+380
-10
lines changed

9 files changed

+380
-10
lines changed

apps/users/api/user.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from drf_spectacular.utils import OpenApiParameter
1111

1212
from common.mixins.api_mixin import APIMixin
13-
from common.result import ResultSerializer
13+
from common.result import ResultSerializer, DefaultResultSerializer
1414
from users.serializers.user import UserProfileResponse, CreateUserSerializer, UserManageSerializer, \
15-
UserInstanceSerializer
15+
UserInstanceSerializer, RePasswordSerializer, CheckCodeSerializer, SendEmailSerializer
1616
from django.utils.translation import gettext_lazy as _
1717
from rest_framework import serializers
1818

@@ -192,3 +192,29 @@ def get_parameters():
192192
# 指定必须给
193193
required=True,
194194
)]
195+
196+
197+
class ResetPasswordAPI(APIMixin):
198+
@staticmethod
199+
def get_request():
200+
return RePasswordSerializer
201+
202+
203+
class CheckCodeAPI(APIMixin):
204+
@staticmethod
205+
def get_request():
206+
return CheckCodeSerializer
207+
208+
@staticmethod
209+
def get_response():
210+
return DefaultResultSerializer
211+
212+
213+
class SendEmailAPI(APIMixin):
214+
@staticmethod
215+
def get_request():
216+
return SendEmailSerializer
217+
218+
@staticmethod
219+
def get_response():
220+
return DefaultResultSerializer

apps/users/serializers/user.py

Lines changed: 175 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
@date:2025/4/14 19:18
77
@desc:
88
"""
9+
import datetime
10+
import os
11+
import random
912
import re
1013
from collections import defaultdict
1114
from itertools import product
12-
15+
from django.core.mail.backends.smtp import EmailBackend
1316
from django.db import transaction
1417
from django.db.models import Q, QuerySet
1518
from rest_framework import serializers
@@ -20,9 +23,13 @@
2023
from common.db.search import page_search
2124
from common.exception.app_exception import AppApiException
2225
from common.utils.common import valid_license, password_encrypt
26+
from maxkb.conf import PROJECT_DIR
27+
from system_manage.models import SystemSetting, SettingType
2328
from users.models import User
24-
from django.utils.translation import gettext_lazy as _
29+
from django.utils.translation import gettext_lazy as _, to_locale
2530
from django.core import validators
31+
from django.core.mail import send_mail
32+
from django.utils.translation import get_language
2633

2734
PASSWORD_REGEX = re.compile(
2835
r"^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~.()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)"
@@ -441,3 +448,169 @@ def update_user_role(instance, user):
441448
workspace_id=workspace_id,
442449
user_id=user.id
443450
)
451+
452+
453+
class RePasswordSerializer(serializers.Serializer):
454+
email = serializers.EmailField(
455+
required=True,
456+
label=_("Email"),
457+
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
458+
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
459+
460+
code = serializers.CharField(required=True, label=_("Verification code"))
461+
462+
password = serializers.CharField(required=True, label=_("Password"),
463+
validators=[validators.RegexValidator(regex=re.compile(
464+
"^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~.()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)"
465+
"(?![0-9_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9_!@#$%^&*`~.()-+=]{6,20}$")
466+
, message=_(
467+
"The confirmation password must be 6-20 characters long and must be a combination of letters, numbers, and special characters."))])
468+
469+
re_password = serializers.CharField(required=True, label=_("Confirm Password"),
470+
validators=[validators.RegexValidator(regex=re.compile(
471+
"^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~.()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)"
472+
"(?![0-9_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9_!@#$%^&*`~.()-+=]{6,20}$")
473+
, message=_(
474+
"The confirmation password must be 6-20 characters long and must be a combination of letters, numbers, and special characters."))]
475+
)
476+
477+
class Meta:
478+
model = User
479+
fields = '__all__'
480+
481+
def is_valid(self, *, raise_exception=False):
482+
super().is_valid(raise_exception=True)
483+
email = self.data.get("email")
484+
# TODO 删除缓存
485+
# cache_code = user_cache.get(email + ':reset_password')
486+
if self.data.get('password') != self.data.get('re_password'):
487+
raise AppApiException(ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.code,
488+
ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.message)
489+
# if cache_code != self.data.get('code'):
490+
# raise AppApiException(ExceptionCodeConstants.CODE_ERROR.value.code,
491+
# ExceptionCodeConstants.CODE_ERROR.value.message)
492+
return True
493+
494+
def reset_password(self):
495+
"""
496+
修改密码
497+
:return: 是否成功
498+
"""
499+
if self.is_valid():
500+
email = self.data.get("email")
501+
QuerySet(User).filter(email=email).update(
502+
password=password_encrypt(self.data.get('password')))
503+
code_cache_key = email + ":reset_password"
504+
# 删除验证码缓存
505+
# user_cache.delete(code_cache_key)
506+
return True
507+
508+
509+
class SendEmailSerializer(serializers.Serializer):
510+
email = serializers.EmailField(
511+
required=True
512+
, label=_("Email"),
513+
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
514+
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
515+
516+
type = serializers.CharField(required=True, label=_("Type"), validators=[
517+
validators.RegexValidator(regex=re.compile("^register|reset_password$"),
518+
message=_("The type only supports register|reset_password"), code=500)
519+
])
520+
521+
class Meta:
522+
model = User
523+
fields = '__all__'
524+
525+
def is_valid(self, *, raise_exception=False):
526+
super().is_valid(raise_exception=raise_exception)
527+
user_exists = QuerySet(User).filter(email=self.data.get('email')).exists()
528+
if not user_exists and self.data.get('type') == 'reset_password':
529+
raise ExceptionCodeConstants.EMAIL_IS_NOT_EXIST.value.to_app_api_exception()
530+
elif user_exists and self.data.get('type') == 'register':
531+
raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception()
532+
code_cache_key = self.data.get('email') + ":" + self.data.get("type")
533+
code_cache_key_lock = code_cache_key + "_lock"
534+
ttl = None # user_cache.ttl(code_cache_key_lock)
535+
if ttl is not None:
536+
raise AppApiException(500, _("Do not send emails again within {seconds} seconds").format(
537+
seconds=int(ttl.total_seconds())))
538+
return True
539+
540+
def send(self):
541+
"""
542+
发送邮件
543+
:return: 是否发送成功
544+
:exception 发送失败异常
545+
"""
546+
email = self.data.get("email")
547+
state = self.data.get("type")
548+
# 生成随机验证码
549+
code = "".join(list(map(lambda i: random.choice(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'
550+
]), range(6))))
551+
# 获取邮件模板
552+
language = get_language()
553+
file = open(
554+
os.path.join(PROJECT_DIR, "apps", "common", 'template', f'email_template_{to_locale(language)}.html'), "r",
555+
encoding='utf-8')
556+
content = file.read()
557+
file.close()
558+
code_cache_key = email + ":" + state
559+
code_cache_key_lock = code_cache_key + "_lock"
560+
# 设置缓存
561+
# user_cache.set(code_cache_key_lock, code, timeout=datetime.timedelta(minutes=1))
562+
system_setting = QuerySet(SystemSetting).filter(type=SettingType.EMAIL.value).first()
563+
if system_setting is None:
564+
# user_cache.delete(code_cache_key_lock)
565+
raise AppApiException(1004,
566+
_("The email service has not been set up. Please contact the administrator to set up the email service in [Email Settings]."))
567+
try:
568+
connection = EmailBackend(system_setting.meta.get("email_host"),
569+
system_setting.meta.get('email_port'),
570+
system_setting.meta.get('email_host_user'),
571+
system_setting.meta.get('email_host_password'),
572+
system_setting.meta.get('email_use_tls'),
573+
False,
574+
system_setting.meta.get('email_use_ssl')
575+
)
576+
# 发送邮件
577+
send_mail(_('【Intelligent knowledge base question and answer system-{action}】').format(
578+
action=_('User registration') if state == 'register' else _('Change password')),
579+
'',
580+
html_message=f'{content.replace("${code}", code)}',
581+
from_email=system_setting.meta.get('from_email'),
582+
recipient_list=[email], fail_silently=False, connection=connection)
583+
except Exception as e:
584+
# user_cache.delete(code_cache_key_lock)
585+
raise AppApiException(500, f"{str(e)}" + _("Email sending failed"))
586+
# user_cache.set(code_cache_key, code, timeout=datetime.timedelta(minutes=30))
587+
return True
588+
589+
590+
class CheckCodeSerializer(serializers.Serializer):
591+
"""
592+
校验验证码
593+
"""
594+
email = serializers.EmailField(
595+
required=True,
596+
label=_("Email"),
597+
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
598+
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
599+
code = serializers.CharField(required=True, label=_("Verification code"))
600+
601+
type = serializers.CharField(required=True,
602+
label=_("Type"),
603+
validators=[
604+
validators.RegexValidator(regex=re.compile("^register|reset_password$"),
605+
message=_(
606+
"The type only supports register|reset_password"),
607+
code=500)
608+
])
609+
610+
def is_valid(self, *, raise_exception=False):
611+
super().is_valid()
612+
#TODO 这里的缓存 需要重新设计
613+
value = None#user_cache.get(self.data.get("email") + ":" + self.data.get("type"))
614+
if value is None or value != self.data.get("code"):
615+
raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception()
616+
return True

apps/users/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
path('user/profile', views.UserProfileView.as_view(), name="user_profile"),
99
path('user/captcha', views.CaptchaView.as_view(), name='captcha'),
1010
path('user/test', views.TestPermissionsUserView.as_view(), name="test"),
11+
path('user/logout', views.Logout.as_view(), name='logout'),
12+
path("user/send_email", views.SendEmail.as_view(), name='send_email'),
13+
path("user/check_code", views.CheckCode.as_view(), name='check_code'),
14+
path("user/re_password", views.RePasswordView.as_view(), name='re_password'),
15+
path("user/current/send_email", views.SendEmailToCurrentUserView.as_view(), name="send_email_current"),
1116
path('workspace/<str:workspace_id>/user_list', views.WorkspaceUserListView.as_view(),
1217
name="test_workspace_id_permission"),
1318
path('workspace/<str:workspace_id>/user/profile', views.TestWorkspacePermissionUserView.as_view(),

apps/users/views/login.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
@date:2025/4/14 10:22
77
@desc:
88
"""
9+
from django.core.cache import cache
910
from django.utils.translation import gettext_lazy as _
1011
from drf_spectacular.utils import extend_schema
1112
from rest_framework.request import Request
1213
from rest_framework.views import APIView
1314

1415
from common import result
16+
from common.auth import TokenAuth
17+
from common.constants.cache_version import Cache_Version
1518
from common.log.log import log
1619
from common.utils.common import encryption
20+
from models_provider.api.model import DefaultModelResponse
1721
from users.api.login import LoginAPI, CaptchaAPI
1822
from users.serializers.login import LoginSerializer, CaptchaSerializer
1923

@@ -44,6 +48,23 @@ def post(self, request: Request):
4448
return result.success(LoginSerializer().login(request.data))
4549

4650

51+
class Logout(APIView):
52+
authentication_classes = [TokenAuth]
53+
54+
@extend_schema(methods=['POST'],
55+
summary=_("Sign out"),
56+
description=_("Sign out"),
57+
operation_id=_("Sign out"), # type: ignore
58+
tags=[_("User Management")], # type: ignore
59+
responses=DefaultModelResponse.get_response())
60+
@log(menu='User management', operate='Sign out',
61+
get_operation_object=lambda r, k: {'name': r.user.username})
62+
def post(self, request: Request):
63+
version, get_key = Cache_Version.TOKEN.value
64+
cache.delete(get_key(token=request.auth), version=version)
65+
return result.success(True)
66+
67+
4768
class CaptchaView(APIView):
4869
@extend_schema(methods=['GET'],
4970
summary=_("Get captcha"),

apps/users/views/user.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
from maxkb.const import CONFIG
2121
from models_provider.api.model import DefaultModelResponse
2222
from tools.serializers.tool import encryption
23+
from users.api import SendEmailAPI, CheckCodeAPI, ResetPasswordAPI
2324
from users.api.user import UserProfileAPI, TestWorkspacePermissionUserApi, DeleteUserApi, EditUserApi, \
2425
ChangeUserPasswordApi, UserPageApi, UserListApi, UserPasswordResponse, WorkspaceUserAPI
2526
from users.models import User
26-
from users.serializers.user import UserProfileSerializer, UserManageSerializer
27+
from users.serializers.user import UserProfileSerializer, UserManageSerializer, CheckCodeSerializer, \
28+
SendEmailSerializer, RePasswordSerializer
2729

2830
default_password = CONFIG.get('default_password', 'MaxKB@123..')
2931

@@ -223,3 +225,73 @@ def get(self, request: Request, current_page, page_size):
223225
data={'email_or_username': request.query_params.get('email_or_username', None),
224226
'user_id': str(request.user.id)})
225227
return result.success(d.page(current_page, page_size))
228+
229+
230+
class RePasswordView(APIView):
231+
232+
@extend_schema(methods=['POST'],
233+
summary=_("Change password"),
234+
description=_("Change password"),
235+
operation_id=_("Change password"), # type: ignore
236+
tags=[_("User Management")], # type: ignore
237+
request=ResetPasswordAPI.get_request(),
238+
responses=DefaultModelResponse.get_response())
239+
@log(menu='User management', operate='Change password',
240+
get_operation_object=lambda r, k: {'name': r.data.get('email', None)},
241+
get_user=lambda r: {'user_name': None, 'email': r.data.get('email', None)},
242+
get_details=get_re_password_details)
243+
def post(self, request: Request):
244+
serializer_obj = RePasswordSerializer(data=request.data)
245+
return result.success(serializer_obj.reset_password())
246+
247+
248+
class SendEmail(APIView):
249+
250+
@extend_schema(methods=['POST'],
251+
summary=_("Send email"),
252+
description=_("Send email"),
253+
operation_id=_("Send email"), # type: ignore
254+
tags=[_("User Management")], # type: ignore
255+
request=SendEmailAPI().get_request(),
256+
responses=SendEmailAPI().get_response())
257+
@log(menu='User management', operate='Send email',
258+
get_operation_object=lambda r, k: {'name': r.data.get('email', None)},
259+
get_user=lambda r: {'user_name': None, 'email': r.data.get('email', None)})
260+
def post(self, request: Request):
261+
serializer_obj = SendEmailSerializer(data=request.data)
262+
if serializer_obj.is_valid(raise_exception=True):
263+
return result.success(serializer_obj.send())
264+
265+
266+
class CheckCode(APIView):
267+
268+
@extend_schema(methods=['POST'],
269+
summary=_("Check whether the verification code is correct"),
270+
description=_("Check whether the verification code is correct"),
271+
operation_id=_("Check whether the verification code is correct"), # type: ignore
272+
tags=[_("User Management")], # type: ignore
273+
request=CheckCodeAPI().get_request(),
274+
responses=CheckCodeAPI().get_response())
275+
@log(menu='User management', operate='Check whether the verification code is correct',
276+
get_operation_object=lambda r, k: {'name': r.data.get('email', None)},
277+
get_user=lambda r: {'user_name': None, 'email': r.data.get('email', None)})
278+
def post(self, request: Request):
279+
return result.success(CheckCodeSerializer(data=request.data).is_valid(raise_exception=True))
280+
281+
282+
class SendEmailToCurrentUserView(APIView):
283+
authentication_classes = [TokenAuth]
284+
285+
@extend_schema(methods=['POST'],
286+
summary=_("Send email to current user"),
287+
description=_("Send email to current user"),
288+
operation_id=_("Send email to current user"), # type: ignore
289+
tags=[_("User Management")], # type: ignore
290+
request=SendEmailAPI().get_request(),
291+
responses=SendEmailAPI().get_response())
292+
@log(menu='User management', operate='Send email to current user',
293+
get_operation_object=lambda r, k: {'name': r.user.username})
294+
def post(self, request: Request):
295+
serializer_obj = SendEmailSerializer(data={'email': request.user.email, 'type': "reset_password"})
296+
if serializer_obj.is_valid(raise_exception=True):
297+
return result.success(serializer_obj.send())

0 commit comments

Comments
 (0)