diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index 22260c9..d0cbedd 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -12,24 +12,23 @@ @file: blog_signals.py @time: 2017/8/12 上午10:18 """ -import django +import _thread +import logging + import django.dispatch from django.dispatch import receiver from django.conf import settings from django.contrib.admin.models import LogEntry -from DjangoBlog.utils import get_current_site from django.core.mail import EmailMultiAlternatives from django.db.models.signals import post_save -from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed +from django.contrib.auth.signals import user_logged_in, user_logged_out -from DjangoBlog.utils import cache, send_email, expire_view_cache, delete_sidebar_cache, delete_view_cache -from DjangoBlog.spider_notify import SpiderNotify from oauth.models import OAuthUser -from blog.models import Article, Category, Tag, Links, SideBar, BlogSettings from comments.models import Comment from comments.utils import send_comment_email -import _thread -import logging +from DjangoBlog.utils import get_current_site +from DjangoBlog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache +from DjangoBlog.spider_notify import SpiderNotify logger = logging.getLogger(__name__) @@ -61,7 +60,7 @@ def send_email_signal_handler(sender, **kwargs): result = msg.send() log.send_result = result > 0 except Exception as e: - logger.error(e) + logger.error(f"失败邮箱号: {emailto}, {e}") log.send_result = False log.save() diff --git a/accounts/email.py b/accounts/email.py new file mode 100644 index 0000000..6b56241 --- /dev/null +++ b/accounts/email.py @@ -0,0 +1,51 @@ +import typing +import random +import string +from datetime import timedelta + +from django.core.cache import cache +from DjangoBlog.utils import send_email + +_code_ttl = timedelta(minutes=5) + + +def generate_code() -> str: + """生成随机数验证码""" + return ''.join(random.sample(string.digits, 6)) + + +def send(to_mail: str, code: str, subject: str = "邮件验证码"): + """发送重设密码验证码 + Args: + to_mail: 接受邮箱 + subject: 邮件主题 + code: 验证码 + """ + html_content = f"您正在重设密码,验证码为:{code}, 5分钟内有效,请妥善保管" + send_email([to_mail], subject, html_content) + + +def verify(email: str, code: str) -> typing.Optional[str]: + """验证code是否有效 + Args: + email: 请求邮箱 + code: 验证码 + Return: + 如果有错误就返回错误str + Node: + 这里的错误处理不太合理,应该采用raise抛出 + 否测调用方也需要对error进行处理 + """ + cache_code = get_code(email) + if cache_code != code: + return "验证码错误" + + +def set_code(email: str, code: str): + """设置code""" + cache.set(email, code, _code_ttl.seconds) + + +def get_code(email: str) -> typing.Optional[str]: + """获取code""" + return cache.get(email) diff --git a/accounts/forms.py b/accounts/forms.py index 1f60cd4..5808946 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -12,11 +12,14 @@ @file: forms.py @time: 2016/11/20 下午3:16 """ -from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django import forms from django.forms import widgets -from django.conf import settings -from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError +from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm + +from . import email +from .models import BlogUser class LoginForm(AuthenticationForm): @@ -50,3 +53,79 @@ class RegisterForm(UserCreationForm): class Meta: model = get_user_model() fields = ("username", "email") + + +class ForgetPasswordForm(forms.Form): + new_password1 = forms.CharField( + label="新密码", + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': "密码" + } + ), + ) + + new_password2 = forms.CharField( + label="确认密码", + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': "确认密码" + } + ), + ) + + email = forms.EmailField( + label='邮箱', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': "邮箱" + } + ), + ) + + code = forms.CharField( + label='验证码', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': "验证码" + } + ), + ) + + def clean_new_password2(self): + password1 = self.data.get("new_password1") + password2 = self.data.get("new_password2") + if password1 and password2 and password1 != password2: + raise ValidationError("两次密码不一致") + password_validation.validate_password(password2) + + return password2 + + def clean_email(self): + user_email = self.cleaned_data.get("email") + if not BlogUser.objects.filter( + email=user_email + ).exists(): + # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + raise ValidationError("未找到邮箱对应的用户") + return user_email + + def clean_code(self): + code = self.cleaned_data.get("code") + error = email.verify( + email=self.cleaned_data.get("email"), + code=code, + ) + if error: + raise ValidationError(error) + return code + + +class ForgetPasswordCodeForm(forms.Form): + email = forms.EmailField( + label="邮箱号" + ) diff --git a/accounts/tests.py b/accounts/tests.py index f3613ef..5fe2ef1 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,12 +1,12 @@ -from django.test import Client, RequestFactory, TestCase -from blog.models import Article, Category, Tag -from django.contrib.auth import get_user_model -from DjangoBlog.utils import delete_view_cache, delete_sidebar_cache -from accounts.models import BlogUser from django.urls import reverse -from DjangoBlog.utils import * from django.conf import settings from django.utils import timezone +from django.test import Client, RequestFactory, TestCase + +from accounts.models import BlogUser +from DjangoBlog.utils import * +from blog.models import Article, Category +from . import email # Create your tests here. @@ -15,6 +15,12 @@ class AccountTest(TestCase): def setUp(self): self.client = Client() self.factory = RequestFactory() + self.blog_user = BlogUser.objects.create_user( + username="test", + email="admin@admin.com", + password="12345678" + ) + self.new_test = "xxx123--=" def test_validate_account(self): site = get_current_site().domain @@ -111,3 +117,101 @@ class AccountTest(TestCase): response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) + + def test_verify_email_code(self): + to_email = "admin@admin.com" + code = email.generate_code() + email.set_code(to_email, code) + email.send(to_email, code) + + err = email.verify("admin@admin.com", code) + self.assertEqual(err, None) + + err = email.verify("admin@123.com", code) + self.assertEqual(type(err), str) + + def test_forget_password_email_code_success(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@admin.com") + ) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.content.decode("utf-8"), "ok") + + def test_forget_password_email_code_fail(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict() + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@com") + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + def test_forget_password_email_success(self): + code = email.generate_code() + email.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code=code, + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + self.assertEqual(resp.status_code, 302) + + # 验证用户密码是否修改成功 + blog_user = BlogUser.objects.filter( + email=self.blog_user.email, + ).first() # type: BlogUser + self.assertNotEqual(blog_user, None) + self.assertEqual(blog_user.check_password(data["new_password1"]), True) + + def test_forget_password_email_not_user(self): + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email="123@123.com", + code="123456", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + self.assertFormError( + response=resp, + form="form", + field="email", + errors="未找到邮箱对应的用户" + ) + + def test_forget_password_email_code_error(self): + code = email.generate_code() + email.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code="111111", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + self.assertFormError( + response=resp, + form="form", + field="code", + errors="验证码错误" + ) diff --git a/accounts/urls.py b/accounts/urls.py index a10ff00..e84123b 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -14,10 +14,10 @@ """ from django.conf.urls import url -from django.contrib.auth import views as auth_view from django.urls import path -from . import views + from .forms import LoginForm +from . import views app_name = "accounts" @@ -33,4 +33,11 @@ urlpatterns = [url(r'^login/$', name='logout'), path(r'account/result.html', views.account_result, - name='result')] + name='result'), + url(r'^forget_password/$', + views.ForgetPasswordView.as_view(), + name='forget_password'), + url(r'^forget_password_code/$', + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), + ] diff --git a/accounts/views.py b/accounts/views.py index 5f7499a..1314449 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,24 +1,31 @@ -from django.shortcuts import render import logging -from .forms import RegisterForm, LoginForm -from django.contrib.auth import authenticate, login, logout -# from django.views.generic.edit import FormView +import threading + +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import render +from django.contrib.auth import logout from django.views.generic import FormView, RedirectView from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 from django.http import HttpResponseRedirect, HttpResponseForbidden from django.urls import reverse -from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth import REDIRECT_FIELD_NAME from django.views.decorators.csrf import csrf_protect from django.contrib import auth from django.views.decorators.cache import never_cache -from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views.decorators.debug import sensitive_post_parameters from django.utils.http import is_safe_url -from DjangoBlog.utils import send_email, get_sha256, get_current_site from django.conf import settings +from django.views import View +from django.contrib.auth.hashers import make_password + +from DjangoBlog.utils import send_email, get_sha256, get_current_site +from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm +from . import email +from .models import BlogUser logger = logging.getLogger(__name__) @@ -166,3 +173,35 @@ def account_result(request): }) else: return HttpResponseRedirect('/') + + +class ForgetPasswordView(FormView): + form_class = ForgetPasswordForm + template_name = 'account/forget_password.html' + + def form_valid(self, form): + if form.is_valid(): + blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() + blog_user.password = make_password(form.cleaned_data["new_password2"]) + blog_user.save() + return HttpResponseRedirect('/login/') + else: + return self.render_to_response({'form': form}) + + +class ForgetPasswordEmailCode(View): + + def post(self, request: HttpRequest): + form = ForgetPasswordCodeForm(request.POST) + if not form.is_valid(): + return HttpResponse("错误的邮箱") + to_email = form.cleaned_data["email"] + + code = email.generate_code() + email.set_code(to_email, code) + + # 异步执行 + t = threading.Thread(target=email.send, args=(to_email, code)) + t.start() + + return HttpResponse("ok") diff --git a/blog/static/account/css/account.css b/blog/static/account/css/account.css new file mode 100644 index 0000000..7d4cec7 --- /dev/null +++ b/blog/static/account/css/account.css @@ -0,0 +1,9 @@ +.button { + border: none; + padding: 4px 80px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; +} \ No newline at end of file diff --git a/blog/static/account/js/account.js b/blog/static/account/js/account.js new file mode 100644 index 0000000..f1a8771 --- /dev/null +++ b/blog/static/account/js/account.js @@ -0,0 +1,47 @@ +let wait = 60; + +function time(o) { + if (wait == 0) { + o.removeAttribute("disabled"); + o.value = "获取验证码"; + wait = 60 + return false + } else { + o.setAttribute("disabled", true); + o.value = "重新发送(" + wait + ")"; + wait--; + setTimeout(function () { + time(o) + }, + 1000) + } +} + +document.getElementById("btn").onclick = function () { + let id_email = $("#id_email") + let token = $("*[name='csrfmiddlewaretoken']").val() + let ts = this + let myErr = $("#myErr") + $.ajax( + { + url: "/forget_password_code/", + type: "POST", + data: { + "email": id_email.val(), + "csrfmiddlewaretoken": token + }, + success: function (result) { + if (result != "ok") { + myErr.remove() + id_email.after("") + return + } + myErr.remove() + time(ts) + }, + error: function (e) { + alert("发送失败,请重试") + } + } + ); +} diff --git a/templates/account/forget_password.html b/templates/account/forget_password.html new file mode 100644 index 0000000..1016c14 --- /dev/null +++ b/templates/account/forget_password.html @@ -0,0 +1,29 @@ +{% extends 'share_layout/base_account.html' %} +{% load static %} +{% block content %} +
+ +

忘记密码

+ +
+ + +
+ +

+ Home Page + | + login page +

+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/account/login.html b/templates/account/login.html index 0a10a60..1773896 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -37,6 +37,8 @@ Create Account | Home Page + | + 忘记密码

diff --git a/templates/share_layout/base_account.html b/templates/share_layout/base_account.html index d7160b5..c00d842 100644 --- a/templates/share_layout/base_account.html +++ b/templates/share_layout/base_account.html @@ -11,6 +11,7 @@ {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + {% load compress %} {% compress css %} @@ -41,4 +42,6 @@ + + \ No newline at end of file