From 8091aa1d61c8638d9061de3cc5698292c9fb9470 Mon Sep 17 00:00:00 2001 From: Tianyi Pu <912396513@qq.com> Date: Mon, 14 Oct 2024 13:30:09 +0100 Subject: [PATCH 01/14] =?UTF-8?q?=E9=87=8D=E5=81=9A=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改的太多了,PR细说 --- back_end/saolei/config/global_settings.py | 2 +- back_end/saolei/userprofile/forms.py | 2 + back_end/saolei/userprofile/urls.py | 2 +- back_end/saolei/userprofile/utils.py | 7 +- back_end/saolei/userprofile/views.py | 94 ++-- back_end/saolei/utils/__init__.py | 8 +- front_end/package.json | 7 +- front_end/src/components/Login.vue | 410 +----------------- .../src/components/dialogs/LoginDialog.vue | 116 +++++ .../src/components/dialogs/RegisterDialog.vue | 144 ++++++ .../src/components/dialogs/RetrieveDialog.vue | 80 ++++ .../components/formItems/emailCodeBlock.vue | 155 +++++++ .../components/formItems/emailFormItem.vue | 53 +++ .../formItems/passwordConfirmBlock.vue | 46 ++ front_end/src/i18n/locales/en.ts | 6 +- front_end/src/i18n/locales/zh-cn.ts | 35 +- front_end/src/utils/common/elFormValidate.ts | 9 + front_end/src/utils/strings.ts | 3 + front_end/src/utils/system/status.ts | 5 +- 19 files changed, 728 insertions(+), 456 deletions(-) create mode 100644 front_end/src/components/dialogs/LoginDialog.vue create mode 100644 front_end/src/components/dialogs/RegisterDialog.vue create mode 100644 front_end/src/components/dialogs/RetrieveDialog.vue create mode 100644 front_end/src/components/formItems/emailCodeBlock.vue create mode 100644 front_end/src/components/formItems/emailFormItem.vue create mode 100644 front_end/src/components/formItems/passwordConfirmBlock.vue create mode 100644 front_end/src/utils/common/elFormValidate.ts create mode 100644 front_end/src/utils/strings.ts diff --git a/back_end/saolei/config/global_settings.py b/back_end/saolei/config/global_settings.py index 932e72a4..86e0dfbf 100644 --- a/back_end/saolei/config/global_settings.py +++ b/back_end/saolei/config/global_settings.py @@ -18,7 +18,7 @@ class MaxSizes: password = 20 # 密码 signature = 4095 # 个性签名的长度,考虑了一些比较啰嗦的语言。 software = 1 - username = 255 # 用户名,考虑了一些名字特别长的文化。 + username = 30 # 用户名,行业习惯的上限 videofile = 5*1024*1024 # 录像文件 # 默认修改个人资料的次数 diff --git a/back_end/saolei/userprofile/forms.py b/back_end/saolei/userprofile/forms.py index 8e9ef92a..bf2e2553 100644 --- a/back_end/saolei/userprofile/forms.py +++ b/back_end/saolei/userprofile/forms.py @@ -19,6 +19,8 @@ class UserLoginForm(forms.Form): # 获取邮箱验证码时的表单,检查邮箱格式用 class EmailForm(forms.Form): email = forms.EmailField(max_length=MaxSizes.email, required=True,error_messages = FormErrors.email) + captcha = forms.CharField(required=True) + hashkey = forms.CharField(required=True) # 注册表单 diff --git a/back_end/saolei/userprofile/urls.py b/back_end/saolei/userprofile/urls.py index cc671d3f..e8671d0a 100644 --- a/back_end/saolei/userprofile/urls.py +++ b/back_end/saolei/userprofile/urls.py @@ -16,7 +16,7 @@ path('get_email_captcha/',views.get_email_captcha), path('get/',views.get_userProfile), path('set/',views.set_userProfile), - + path('checkcollision/',views.check_collision), # path('captcha/captcha', views.captcha, name='captcha'), # path('edit//', views.profile_edit, name='edit'), diff --git a/back_end/saolei/userprofile/utils.py b/back_end/saolei/userprofile/utils.py index 21e4b912..71ff5f25 100644 --- a/back_end/saolei/userprofile/utils.py +++ b/back_end/saolei/userprofile/utils.py @@ -17,7 +17,10 @@ def judge_captcha(captchaStr, captchaHashkey): def judge_email_verification(email, email_captcha, emailHashkey): get_email_captcha = EmailVerifyRecord.objects.filter(hashkey=emailHashkey).first() - if (timezone.now() - get_email_captcha.send_time).seconds <= 3600: + print(get_email_captcha) + if not get_email_captcha or not email_captcha: + return False + if (timezone.now() - get_email_captcha.send_time).seconds > 3600: EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() return False - return get_email_captcha and email_captcha and get_email_captcha.code == email_captcha and get_email_captcha.email == email \ No newline at end of file + return get_email_captcha.code == email_captcha and get_email_captcha.email == email \ No newline at end of file diff --git a/back_end/saolei/userprofile/views.py b/back_end/saolei/userprofile/views.py index 47eeb4e8..645a8bb2 100644 --- a/back_end/saolei/userprofile/views.py +++ b/back_end/saolei/userprofile/views.py @@ -1,7 +1,7 @@ import logging logger = logging.getLogger('userprofile') from django.contrib.auth import authenticate, login, logout -from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, HttpResponseNotFound +from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, HttpResponseNotFound, HttpResponseBadRequest from .forms import UserLoginForm, UserRegisterForm, UserRetrieveForm, EmailForm from captcha.models import CaptchaStore import json @@ -24,34 +24,29 @@ # 用账号、密码登录 # 此处要分成两个,密码容易碰撞,hash难碰撞 def user_login(request): - user_login_form = UserLoginForm(data=request.POST) - if not user_login_form.is_valid(): - return JsonResponse({'status': 106, 'msg': "表单错误!"}) - data = user_login_form.cleaned_data - + data = request.POST + if not UserLoginForm(data=data).is_valid(): + return HttpResponseBadRequest() capt = data["captcha"] # 用户提交的验证码 key = data["hashkey"] # 验证码hash username = data["username"] - response = {'status': 100, 'msg': None} if not judge_captcha(capt, key): logger.info(f'用户 {username} 验证码错误') - return JsonResponse({'status': 104, 'msg': "验证码错误!"}) + return JsonResponse({'type': 'error', 'object': 'login', 'category': 'captcha'}) # 检验账号、密码是否正确匹配数据库中的某个用户 # 如果均匹配则返回这个 user 对象 user = authenticate( username=username, password=data['password']) if not user: logger.info(f'用户 {username} 账密错误') - return JsonResponse({'status': 105, 'msg': "账号或密码输入有误。请重新输入~"}) + return JsonResponse({'type': 'error', 'object': 'login', 'category': 'password'}) # 将用户数据保存在 session 中,即实现了登录动作 login(request, user) - response['msg'] = { - "id": user.id, "username": user.username, - "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} + userdata = {"id": user.id, "username": user.username, "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} if 'user_id' in data and data['user_id'] != str(user.id): # 检测到小号 - logger.warning(f'{data["user_id"][:50]} is diffrent from {str(user.id)}.') - return JsonResponse(response) + logger.warning(f'{data["user_id"][:50]} is different from {str(user.id)}.') + return JsonResponse({'type': 'success', 'user': userdata}) @require_GET @@ -72,15 +67,14 @@ def user_logout(request): def user_retrieve(request): user_retrieve_form = UserRetrieveForm(data=request.POST) if not user_retrieve_form.is_valid(): - return JsonResponse({'status': 101, 'msg': user_retrieve_form.errors.\ - as_text().split("*")[-1]}) + return HttpResponseBadRequest() emailHashkey = request.POST.get("email_key") email_captcha = request.POST.get("email_captcha") email = request.POST.get("email") if judge_email_verification(email, email_captcha, emailHashkey): user = UserProfile.objects.filter(email=user_retrieve_form.cleaned_data['email']).first() if not user: - return JsonResponse({'status': 109, 'msg': "该邮箱尚未注册,请先注册!"}) + return HttpResponseNotFound() # 前端已经查过重了,理论上不应该进到这里 # 设置密码(哈希) user.set_password( user_retrieve_form.cleaned_data['password']) @@ -89,9 +83,10 @@ def user_retrieve(request): login(request, user) logger.info(f'用户 {user.username}#{user.id} 邮箱找回密码') EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 100, 'msg': user.realname}) + userdata = {"id": user.id, "username": user.username, "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} + return JsonResponse({'type': 'success', 'user': userdata}) else: - return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确或已过期!"}) + return JsonResponse({'type': 'error', 'object': 'emailcode'}) # 用户注册 @@ -104,7 +99,8 @@ def user_register(request): emailHashkey = request.POST.get("email_key") email_captcha = request.POST.get("email_captcha") email = request.POST.get("email") - if EMAIL_SKIP or judge_email_verification(email, email_captcha, emailHashkey): + print(email, email_captcha, emailHashkey) + if judge_email_verification(email, email_captcha, emailHashkey): new_user = user_register_form.save(commit=False) # 设置密码(哈希) new_user.set_password( @@ -119,12 +115,12 @@ def user_register(request): logger.info(f'用户 {new_user.username}#{new_user.id} 注册') # 顺手把过期的验证码删了 EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 100, 'msg': { + return JsonResponse({'type': 'success', 'user': { "id": new_user.id, "username": new_user.username, - "realname": new_user.realname, "is_banned": False} + "realname": new_user.realname, "is_banned": new_user.is_banned, "is_staff": new_user.is_staff} }) else: - return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确或已过期!"}) + return JsonResponse({'type': 'error', 'object': 'emailcode'}) else: if "email" not in user_register_form.cleaned_data or "username" not in user_register_form.cleaned_data: # 可能发生前端验证正确,而后端验证不正确(后端更严格),此时clean会直接删除email字段 @@ -136,6 +132,19 @@ def user_register(request): return JsonResponse({'status': 101, 'msg': user_register_form.errors.\ as_text().split("*")[-1]}) +@require_GET +def check_collision(request): + user = None + if request.GET.get('username'): + print(request.GET.get('username')) + user = UserProfile.objects.filter(username=request.GET.get('username')).first() + elif request.GET.get('email'): + user = UserProfile.objects.filter(email=request.GET.get('email')).first() + else: + return HttpResponseBadRequest() + if not user: + return HttpResponse(False) + return HttpResponse(True) # 【站长】任命解除管理员 # http://127.0.0.1:8000/userprofile/set_staff/?id=1&is_staff=True @@ -197,27 +206,24 @@ def refresh_captcha(request): @ratelimit(key='ip', rate='20/h') @require_POST def get_email_captcha(request): - email_form = EmailForm(data=request.POST) - if email_form.is_valid(): - capt = request.POST.get("captcha", None) # 用户提交的验证码 - key = request.POST.get("hashkey", None) # 验证码hash - response = {'status': 100, 'msg': None, "hashkey": None} - if judge_captcha(capt, key): - hashkey = send_email(request.POST.get("email", None), request.POST.get("type", None)) - if hashkey: - response['hashkey'] = hashkey - return JsonResponse(response) - else: - response['status'] = 103 - response['msg'] = "发送邮件失败" - return JsonResponse(response) - else: - response['status'] = 104 - response['msg'] = "验证码错误" - return JsonResponse(response) - else: - return JsonResponse({'status': 110, 'msg': email_form.errors.\ - as_text().split("*")[-1]}) + data = request.POST + email_form = EmailForm(data=data) + if not email_form.is_valid(): # 正常工作的前端不应当发出的请求 + return HttpResponseBadRequest() + capt = data.get("captcha") + key = data.get("hashkey") + if not judge_captcha(capt, key): # 图形验证码不对 + return JsonResponse({'type': 'error', 'object': 'captcha'}) + if EMAIL_SKIP: + code, hashkey = send_email(data.get("email"), data.get("type")) + return JsonResponse({'type': 'success', 'code': code, 'hashkey': hashkey}) + hashkey = send_email(data.get("email"), data.get("type")) + if hashkey: # 邮件发送成功 + return JsonResponse({'type': 'success', 'hashkey': hashkey}) + else: # 邮件发送失败 + return JsonResponse({'type': 'error', 'object': 'email'}) + + # 管理员使用的操作接口,调用方式见前端的StaffView.vue get_userProfile_fields = ["id", "userms__identifiers", "userms__video_num_limit", "username", "first_name", "last_name", "email", "realname", "signature", "country", "left_realname_n", "left_avatar_n", "left_signature_n", "is_banned"] # 可获取的域列表 diff --git a/back_end/saolei/utils/__init__.py b/back_end/saolei/utils/__init__.py index ebeef93f..ea9b8b2b 100644 --- a/back_end/saolei/utils/__init__.py +++ b/back_end/saolei/utils/__init__.py @@ -6,7 +6,7 @@ from django.http import HttpResponse, JsonResponse, FileResponse from django.shortcuts import render, redirect import requests -from config.flags import BAIDU_VERIFY_SKIP +from config.flags import BAIDU_VERIFY_SKIP, EMAIL_SKIP def generate_code(code_len): """ @@ -34,7 +34,9 @@ def send_email(email, send_type='register'): # email_record.send_type = send_type email_record.save() -# 验证码保存之后,我们就要把带有验证码的链接发送到注册时的邮箱! + # 验证码保存之后,我们就要把带有验证码的链接发送到注册时的邮箱! + if EMAIL_SKIP: + return code, hashkey if send_type == 'register': email_title = '元扫雷网邮箱注册验证码' email_body = f'欢迎您注册元扫雷网,您的邮箱验证码为:{code}(一小时内有效)。' @@ -45,7 +47,7 @@ def send_email(email, send_type='register'): return None send_status = send_mail(email_title, email_body, 'wangjianing@88.com', [email]) if send_status: - return hashkey + return code, hashkey else: return None diff --git a/front_end/package.json b/front_end/package.json index d9411707..6768d7f2 100644 --- a/front_end/package.json +++ b/front_end/package.json @@ -10,6 +10,7 @@ "build:openms": "vite build --mode openms" }, "dependencies": { + "@chenfengyuan/vue-countdown": "^2.1.2", "@element-plus/icons-vue": "^2.1.0", "@mdit/plugin-abbr": "^0.10.0", "@mdit/plugin-align": "^0.10.0", @@ -22,7 +23,7 @@ "@vueuse/core": "^10.11.0", "axios": "^1.7.2", "echarts": "^5.5.0", - "element-plus": "^2.7.0", + "element-plus": "^2.8.0", "flag-icon-css": "^4.1.7", "highlight.js": "^11.9.0", "image-conversion": "^2.1.1", @@ -32,10 +33,12 @@ "markdown-it-highlightjs": "^4.0.1", "markdown-it-mathjax3": "^4.3.2", "ms-toollib": "^1.4.10", + "out-of-character": "^1.2.2", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "uuid": "^9.0.0", - "vue": "^3.4.0", + "validator": "^13.12.0", + "vue": "^3.5.12", "vue-echarts": "^6.7.1", "vue-i18n": "^9.13.1", "vue-router": "^4.0.3" diff --git a/front_end/src/components/Login.vue b/front_end/src/components/Login.vue index 88bc24ff..34e33620 100644 --- a/front_end/src/components/Login.vue +++ b/front_end/src/components/Login.vue @@ -1,9 +1,9 @@ diff --git a/front_end/src/components/dialogs/LoginDialog.vue b/front_end/src/components/dialogs/LoginDialog.vue new file mode 100644 index 00000000..b967d22e --- /dev/null +++ b/front_end/src/components/dialogs/LoginDialog.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/dialogs/RegisterDialog.vue b/front_end/src/components/dialogs/RegisterDialog.vue new file mode 100644 index 00000000..559514c2 --- /dev/null +++ b/front_end/src/components/dialogs/RegisterDialog.vue @@ -0,0 +1,144 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/dialogs/RetrieveDialog.vue b/front_end/src/components/dialogs/RetrieveDialog.vue new file mode 100644 index 00000000..307412a8 --- /dev/null +++ b/front_end/src/components/dialogs/RetrieveDialog.vue @@ -0,0 +1,80 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/formItems/emailCodeBlock.vue b/front_end/src/components/formItems/emailCodeBlock.vue new file mode 100644 index 00000000..c98b5279 --- /dev/null +++ b/front_end/src/components/formItems/emailCodeBlock.vue @@ -0,0 +1,155 @@ + + + + \ No newline at end of file diff --git a/front_end/src/components/formItems/emailFormItem.vue b/front_end/src/components/formItems/emailFormItem.vue new file mode 100644 index 00000000..cf9519b1 --- /dev/null +++ b/front_end/src/components/formItems/emailFormItem.vue @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/formItems/passwordConfirmBlock.vue b/front_end/src/components/formItems/passwordConfirmBlock.vue new file mode 100644 index 00000000..c22a54d3 --- /dev/null +++ b/front_end/src/components/formItems/passwordConfirmBlock.vue @@ -0,0 +1,46 @@ + + + \ No newline at end of file diff --git a/front_end/src/i18n/locales/en.ts b/front_end/src/i18n/locales/en.ts index 0d258cd3..327e4a4e 100644 --- a/front_end/src/i18n/locales/en.ts +++ b/front_end/src/i18n/locales/en.ts @@ -153,9 +153,9 @@ export const en = { }, login: { title: 'Login', - username: 'username', - password: 'password', - captcha: 'captcha', + username: 'Username', + password: 'Password', + captcha: 'Captcha', forgetPassword: 'Forget password?', keepMeLoggedIn: 'Keep me logged in', confirm: 'Log in' diff --git a/front_end/src/i18n/locales/zh-cn.ts b/front_end/src/i18n/locales/zh-cn.ts index d75086df..f73ee9e8 100644 --- a/front_end/src/i18n/locales/zh-cn.ts +++ b/front_end/src/i18n/locales/zh-cn.ts @@ -132,6 +132,10 @@ export const zhCn = { password: '请输入新的6-20位密码', success: '修改密码成功!', }, + form: { + email: '邮箱', + username: '用户名', + }, guide: { announcement: '公告', other: '其他', @@ -155,11 +159,12 @@ export const zhCn = { login: { title: '欢迎登录', username: '用户名', + usernameRequired: '请输入用户名!', password: '密码', captcha: '验证码', forgetPassword: '(找回密码)', keepMeLoggedIn: '记住我', - confirm: '登录' + confirm: '登录', }, menu: { ranking: '排行榜', @@ -226,13 +231,13 @@ export const zhCn = { }, register: { title: '用户注册', - username: '请输入用户昵称(唯一、登录凭证、无法修改)', - email: '请输入邮箱(唯一)', + username: '用户名', + email: '邮箱', captcha: '验证码', getEmailCode: '获取邮箱验证码', - emailCode: '请输入邮箱验证码', - password: '请输入6-20位密码', - confirmPassword: '请输入确认密码', + emailCode: '邮箱验证码', + password: '密码', + confirmPassword: '确认密码', agreeTo: '已阅读并同意', termsAndConditions: '开源扫雷网用户协议', confirm: '注册', @@ -269,4 +274,22 @@ export const zhCn = { designer: '外观设计', acknowledgement: '致谢', }, + validator: { + captchaFail: '验证码不正确,请重新输入', + captchaRefresh: '请重新输入', + captchaRequired: '请输入图形验证码', + confirmPasswordMismatch: '密码和确认密码不一致', + connectionFail: '无法连接到服务器,请重试', + emailCodeRequired: '请输入邮箱验证码', + emailCollision: '邮箱已存在', + emailInvalid: '邮箱格式错误', + emailNoCollision: '邮箱未注册', + emailRequired: '请输入邮箱', + illegalCharacter: '非法字符', + passwordMinimum: '密码至少6位', + unknownError: '发生未知错误,请联系开发者。{0}', + usernameCollision: '用户名已存在', + usernameInvalid: '用户名必须包含至少一个可见字符', + usernameRequired: '请输入用户名', + } } diff --git a/front_end/src/utils/common/elFormValidate.ts b/front_end/src/utils/common/elFormValidate.ts new file mode 100644 index 00000000..d31051be --- /dev/null +++ b/front_end/src/utils/common/elFormValidate.ts @@ -0,0 +1,9 @@ + +export function validateSuccess(elFormItemRef: any) { + elFormItemRef.value!.validateMessage=''; + elFormItemRef.value!.validateState='success'; +} +export function validateError(elFormItemRef: any, msg: string) { + elFormItemRef.value!.validateMessage=msg; + elFormItemRef.value!.validateState='error'; +} \ No newline at end of file diff --git a/front_end/src/utils/strings.ts b/front_end/src/utils/strings.ts new file mode 100644 index 00000000..2288503e --- /dev/null +++ b/front_end/src/utils/strings.ts @@ -0,0 +1,3 @@ +// \u2028: Line Separator +// \u2029: Paragraph Separator +export const containsControl = /[\x00-\x1F\x7F-\x9F\u2028\u2029]/; diff --git a/front_end/src/utils/system/status.ts b/front_end/src/utils/system/status.ts index a7a52457..476de2b0 100644 --- a/front_end/src/utils/system/status.ts +++ b/front_end/src/utils/system/status.ts @@ -15,11 +15,14 @@ const notificationMessage: { [code: number]: string} = { export function generalNotification(t: any, status: number, action: string) { let type = Math.floor(status / 100); + let local = localStorage.getItem('local') + let duration = 4500 + if (local !== null) duration = JSON.parse(local).notification_duration ElNotification({ title: t.t(notificationTitle[type], [action]), message: t.t(notificationMessage[status]), type: notificationType[type], - duration: JSON.parse(localStorage.getItem('local')).notification_duration, + duration: duration, }) } From 669520e2b0649e3c21c62f84bd9a11cfc6aa172b Mon Sep 17 00:00:00 2001 From: Tianyi Pu <912396513@qq.com> Date: Tue, 15 Oct 2024 09:03:25 +0100 Subject: [PATCH 02/14] =?UTF-8?q?fix,=20=E6=9C=AC=E5=9C=B0=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/dialogs/LoginDialog.vue | 14 +-- .../src/components/dialogs/RegisterDialog.vue | 28 ++--- .../src/components/dialogs/RetrieveDialog.vue | 25 ++++- .../components/formItems/emailCodeBlock.vue | 40 +++---- .../components/formItems/emailFormItem.vue | 16 +-- .../formItems/passwordConfirmBlock.vue | 10 +- front_end/src/i18n/index.ts | 10 +- front_end/src/i18n/locales/de.ts | 41 +++---- front_end/src/i18n/locales/dev.ts | 4 +- front_end/src/i18n/locales/en.ts | 46 ++------ front_end/src/i18n/locales/pl.ts | 35 +++--- front_end/src/i18n/locales/zh-cn.ts | 106 ++++++++---------- 12 files changed, 161 insertions(+), 214 deletions(-) diff --git a/front_end/src/components/dialogs/LoginDialog.vue b/front_end/src/components/dialogs/LoginDialog.vue index b967d22e..ff53a18e 100644 --- a/front_end/src/components/dialogs/LoginDialog.vue +++ b/front_end/src/components/dialogs/LoginDialog.vue @@ -1,17 +1,17 @@