Skip to content

Commit c1ddec1

Browse files
authored
feat: Login and add graphic captcha (#3117)
1 parent 1ba8077 commit c1ddec1

File tree

16 files changed

+161
-17
lines changed

16 files changed

+161
-17
lines changed

apps/common/util/common.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io
1212
import mimetypes
1313
import pickle
14+
import random
1415
import re
1516
import shutil
1617
from functools import reduce
@@ -297,3 +298,10 @@ def markdown_to_plain_text(md: str) -> str:
297298
# 去除首尾空格
298299
text = text.strip()
299300
return text
301+
302+
303+
CHAR_SET = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
304+
305+
306+
def get_random_chars(number=6):
307+
return "".join([CHAR_SET[random.randint(0, len(CHAR_SET) - 1)] for index in range(number)])

apps/locales/en_US/LC_MESSAGES/django.po

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7490,4 +7490,13 @@ msgid "Field: {name} No value set"
74907490
msgstr ""
74917491

74927492
msgid "Generate related"
7493+
msgstr ""
7494+
7495+
msgid "Obtain graphical captcha"
7496+
msgstr ""
7497+
7498+
msgid "Captcha code error or expiration"
7499+
msgstr ""
7500+
7501+
msgid "captcha"
74937502
msgstr ""

apps/locales/zh_CN/LC_MESSAGES/django.po

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7653,4 +7653,13 @@ msgid "Field: {name} No value set"
76537653
msgstr "字段: {name} 未设置值"
76547654

76557655
msgid "Generate related"
7656-
msgstr "生成问题"
7656+
msgstr "生成问题"
7657+
7658+
msgid "Obtain graphical captcha"
7659+
msgstr "获取图形验证码"
7660+
7661+
msgid "Captcha code error or expiration"
7662+
msgstr "验证码错误或过期"
7663+
7664+
msgid "captcha"
7665+
msgstr "验证码"

apps/locales/zh_Hant/LC_MESSAGES/django.po

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7663,4 +7663,13 @@ msgid "Field: {name} No value set"
76637663
msgstr "欄位: {name} 未設定值"
76647664

76657665
msgid "Generate related"
7666-
msgstr "生成問題"
7666+
msgstr "生成問題"
7667+
7668+
msgid "Obtain graphical captcha"
7669+
msgstr "獲取圖形驗證碼"
7670+
7671+
msgid "Captcha code error or expiration"
7672+
msgstr "驗證碼錯誤或過期"
7673+
7674+
msgid "captcha"
7675+
msgstr "驗證碼"

apps/smartdoc/settings/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@
126126
"token_cache": {
127127
'BACKEND': 'common.cache.file_cache.FileCache',
128128
'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "token_cache") # 文件夹路径
129+
},
130+
'captcha_cache': {
131+
'BACKEND': 'common.cache.file_cache.FileCache',
132+
'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "captcha_cache") # 文件夹路径
129133
}
130134
}
131135

apps/users/serializers/user_serializers.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@
66
@date:2023/9/5 16:32
77
@desc:
88
"""
9+
import base64
910
import datetime
1011
import os
1112
import random
1213
import re
1314
import uuid
1415

16+
from captcha.image import ImageCaptcha
1517
from django.conf import settings
1618
from django.core import validators, signing, cache
1719
from django.core.mail import send_mail
1820
from django.core.mail.backends.smtp import EmailBackend
1921
from django.db import transaction
2022
from django.db.models import Q, QuerySet, Prefetch
23+
from django.utils.translation import get_language
24+
from django.utils.translation import gettext_lazy as _, to_locale
2125
from drf_yasg import openapi
2226
from rest_framework import serializers
2327

@@ -30,7 +34,7 @@
3034
from common.mixins.api_mixin import ApiMixin
3135
from common.models.db_model_manage import DBModelManage
3236
from common.response.result import get_api_response
33-
from common.util.common import valid_license
37+
from common.util.common import valid_license, get_random_chars
3438
from common.util.field_message import ErrMessage
3539
from common.util.lock import lock
3640
from dataset.models import DataSet, Document, Paragraph, Problem, ProblemParagraphMapping
@@ -39,9 +43,29 @@
3943
from setting.models import Team, SystemSetting, SettingType, Model, TeamMember, TeamMemberPermission
4044
from smartdoc.conf import PROJECT_DIR
4145
from users.models.user import User, password_encrypt, get_user_dynamics_permission
42-
from django.utils.translation import gettext_lazy as _, gettext, to_locale
43-
from django.utils.translation import get_language
46+
4447
user_cache = cache.caches['user_cache']
48+
captcha_cache = cache.caches['captcha_cache']
49+
50+
51+
class CaptchaSerializer(ApiMixin, serializers.Serializer):
52+
@staticmethod
53+
def get_response_body_api():
54+
return get_api_response(openapi.Schema(
55+
type=openapi.TYPE_STRING,
56+
title="captcha",
57+
default="xxxx",
58+
description="captcha"
59+
))
60+
61+
@staticmethod
62+
def generate():
63+
chars = get_random_chars()
64+
image = ImageCaptcha()
65+
data = image.generate(chars)
66+
captcha = base64.b64encode(data.getbuffer())
67+
captcha_cache.set(f"LOGIN:{chars}", chars, timeout=5 * 60)
68+
return 'data:image/png;base64,' + captcha.decode()
4569

4670

4771
class SystemSerializer(ApiMixin, serializers.Serializer):
@@ -71,13 +95,19 @@ class LoginSerializer(ApiMixin, serializers.Serializer):
7195

7296
password = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Password")))
7397

98+
captcha = serializers.CharField(required=True, error_messages=ErrMessage.char(_("captcha")))
99+
74100
def is_valid(self, *, raise_exception=False):
75101
"""
76102
校验参数
77103
:param raise_exception: Whether to throw an exception can only be True
78104
:return: User information
79105
"""
80106
super().is_valid(raise_exception=True)
107+
captcha = self.data.get('captcha')
108+
captcha_value = captcha_cache.get(f"LOGIN:{captcha}")
109+
if captcha_value is None:
110+
raise AppApiException(1005, _("Captcha code error or expiration"))
81111
username = self.data.get("username")
82112
password = password_encrypt(self.data.get("password"))
83113
user = QuerySet(User).filter(Q(username=username,
@@ -109,7 +139,8 @@ def get_request_body_api(self):
109139
required=['username', 'password'],
110140
properties={
111141
'username': openapi.Schema(type=openapi.TYPE_STRING, title=_("Username"), description=_("Username")),
112-
'password': openapi.Schema(type=openapi.TYPE_STRING, title=_("Password"), description=_("Password"))
142+
'password': openapi.Schema(type=openapi.TYPE_STRING, title=_("Password"), description=_("Password")),
143+
'captcha': openapi.Schema(type=openapi.TYPE_STRING, title=_("captcha"), description=_("captcha"))
113144
}
114145
)
115146

apps/users/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
urlpatterns = [
77
path('profile', views.Profile.as_view()),
88
path('user', views.User.as_view(), name="profile"),
9+
path('user/captcha', views.CaptchaView.as_view(), name='captcha'),
910
path('user/language', views.SwitchUserLanguageView.as_view(), name='language'),
1011
path('user/list', views.User.Query.as_view()),
1112
path('user/login', views.Login.as_view(), name='login'),

apps/users/views/user.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from users.serializers.user_serializers import RegisterSerializer, LoginSerializer, CheckCodeSerializer, \
2727
RePasswordSerializer, \
2828
SendEmailSerializer, UserProfile, UserSerializer, UserManageSerializer, UserInstanceSerializer, SystemSerializer, \
29-
SwitchLanguageSerializer
29+
SwitchLanguageSerializer, CaptchaSerializer
3030
from users.views.common import get_user_operation_object, get_re_password_details
3131

3232
user_cache = cache.caches['user_cache']
@@ -170,6 +170,18 @@ def _get_details(request):
170170
}
171171

172172

173+
class CaptchaView(APIView):
174+
175+
@action(methods=['GET'], detail=False)
176+
@swagger_auto_schema(operation_summary=_("Obtain graphical captcha"),
177+
operation_id=_("Obtain graphical captcha"),
178+
responses=CaptchaSerializer().get_response_body_api(),
179+
security=[],
180+
tags=[_("User management")])
181+
def get(self, request: Request):
182+
return result.success(CaptchaSerializer().generate())
183+
184+
173185
class Login(APIView):
174186

175187
@action(methods=['POST'], detail=False)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ django-db-connection-pool = "1.2.5"
6868
opencv-python-headless = "4.11.0.86"
6969
pymysql = "1.1.1"
7070
accelerate = "1.6.0"
71+
captcha = "0.7.1"
7172
[build-system]
7273
requires = ["poetry-core"]
7374
build-backend = "poetry.core.masonry.api"

ui/src/api/type/user.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ interface LoginRequest {
3737
* 密码
3838
*/
3939
password: string
40+
/**
41+
* 验证码
42+
*/
43+
captcha: string
4044
}
4145

4246
interface RegisterRequest {

ui/src/api/user.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ const login: (
2727
}
2828
return post('/user/login', request, undefined, loading)
2929
}
30+
/**
31+
* 获取图形验证码
32+
* @returns
33+
*/
34+
const getCaptcha: () => Promise<Result<string>> = () => {
35+
return get('user/captcha')
36+
}
3037
/**
3138
* 登出
3239
* @param loading 接口加载器
@@ -226,5 +233,6 @@ export default {
226233
postLanguage,
227234
getDingOauth2Callback,
228235
getlarkCallback,
229-
getQrSource
236+
getQrSource,
237+
getCaptcha
230238
}

ui/src/locales/lang/en-US/views/user.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export default {
2828
requiredMessage: 'Please enter username',
2929
lengthMessage: 'Length must be between 6 and 20 words'
3030
},
31+
captcha: {
32+
label: 'captcha',
33+
placeholder: 'Please enter the captcha'
34+
},
3135
nick_name: {
3236
label: 'Name',
3337
placeholder: 'Please enter name'

ui/src/locales/lang/zh-CN/views/user.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export default {
2525
requiredMessage: '请输入用户名',
2626
lengthMessage: '长度在 6 到 20 个字符'
2727
},
28+
captcha: {
29+
label: '验证码',
30+
placeholder: '请输入验证码'
31+
},
2832
nick_name: {
2933
label: '姓名',
3034
placeholder: '请输入姓名'
@@ -33,7 +37,7 @@ export default {
3337
label: '邮箱',
3438
placeholder: '请输入邮箱',
3539
requiredMessage: '请输入邮箱',
36-
validatorEmail: '请输入有效邮箱格式!',
40+
validatorEmail: '请输入有效邮箱格式!'
3741
},
3842
phone: {
3943
label: '手机号',
@@ -48,13 +52,13 @@ export default {
4852
new_password: {
4953
label: '新密码',
5054
placeholder: '请输入新密码',
51-
requiredMessage: '请输入新密码',
55+
requiredMessage: '请输入新密码'
5256
},
5357
re_password: {
5458
label: '确认密码',
5559
placeholder: '请输入确认密码',
5660
requiredMessage: '请输入确认密码',
57-
validatorMessage: '密码不一致',
61+
validatorMessage: '密码不一致'
5862
}
5963
}
6064
},

ui/src/locales/lang/zh-Hant/views/user.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export default {
2626
requiredMessage: '請輸入使用者名稱',
2727
lengthMessage: '長度須介於 6 到 20 個字元之間'
2828
},
29+
captcha: {
30+
label: '驗證碼',
31+
placeholder: '請輸入驗證碼'
32+
},
2933
nick_name: {
3034
label: '姓名',
3135
placeholder: '請輸入姓名'

ui/src/stores/modules/user.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ const useUserStore = defineStore({
135135
})
136136
},
137137

138-
async login(auth_type: string, username: string, password: string) {
139-
return UserApi.login(auth_type, { username, password }).then((ok) => {
138+
async login(auth_type: string, username: string, password: string, captcha: string) {
139+
return UserApi.login(auth_type, { username, password, captcha }).then((ok) => {
140140
this.token = ok.data
141141
localStorage.setItem('token', ok.data)
142142
return this.profile()

0 commit comments

Comments
 (0)