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/DjangoBlog/utils.py b/DjangoBlog/utils.py index 68f2806..6afd817 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -2,28 +2,21 @@ # encoding: utf-8 -""" -@version: ?? -@author: liangliangyy -@license: MIT Licence -@contact: liangliangyy@gmail.com -@site: https://www.lylinux.net/ -@software: PyCharm -@file: utils.py -@time: 2017/1/19 上午2:30 -""" -from django.core.cache import cache -from django.contrib.sites.models import Site +import logging +import os +import random +import string +import uuid from hashlib import sha256 + import mistune +import requests +from django.contrib.sites.models import Site +from django.core.cache import cache from mistune import escape, escape_link from pygments import highlight -from pygments.lexers import get_lexer_by_name from pygments.formatters import html -import logging -import requests -import uuid -import os +from pygments.lexers import get_lexer_by_name logger = logging.getLogger(__name__) @@ -187,6 +180,11 @@ def send_email(emailto, title, content): content=content) +def generate_code() -> str: + """生成随机数验证码""" + return ''.join(random.sample(string.digits, 6)) + + def parse_dict_to_url(dict): from urllib.parse import quote url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) diff --git a/accounts/forms.py b/accounts/forms.py index 1f60cd4..8e8049d 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -12,11 +12,14 @@ @file: forms.py @time: 2016/11/20 下午3:16 """ +from django import forms +from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.forms import AuthenticationForm, UserCreationForm -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.forms import widgets + +from . import utils +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 = utils.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..f0e7b12 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,13 +1,13 @@ +from django.conf import settings 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 DjangoBlog.utils import * +from accounts.models import BlogUser +from blog.models import Article, Category +from . import utils + # 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 = generate_code() + utils.set_code(to_email, code) + utils.send_verify_email(to_email, code) + + err = utils.verify("admin@admin.com", code) + self.assertEqual(err, None) + + err = utils.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 = generate_code() + utils.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 = generate_code() + utils.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/utils.py b/accounts/utils.py new file mode 100644 index 0000000..cd5c360 --- /dev/null +++ b/accounts/utils.py @@ -0,0 +1,45 @@ +import typing +from datetime import timedelta + +from django.core.cache import cache + +from DjangoBlog.utils import send_email + +_code_ttl = timedelta(minutes=5) + + +def send_verify_email(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/views.py b/accounts/views.py index 8114d67..438337b 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,24 +1,30 @@ -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 -from django.views.generic import FormView, RedirectView + +from django.conf import settings +from django.contrib import auth +from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import get_user_model -from django.shortcuts import get_object_or_404 +from django.contrib.auth import logout +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.hashers import make_password from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404 +from django.shortcuts import render from django.urls import reverse -from django.contrib.auth.forms import AuthenticationForm, UserCreationForm -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.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters +from django.views.generic import FormView, RedirectView + +from DjangoBlog.utils import send_email, get_sha256, get_current_site, generate_code +from . import utils +from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm +from .models import BlogUser logger = logging.getLogger(__name__) @@ -62,7 +68,7 @@ class RegisterView(FormView): content=content) url = reverse('accounts:result') + \ - '?type=register&id=' + str(user.id) + '?type=register&id=' + str(user.id) return HttpResponseRedirect(url) else: return self.render_to_response({ @@ -89,6 +95,7 @@ class LoginView(FormView): template_name = 'account/login.html' success_url = '/' redirect_field_name = REDIRECT_FIELD_NAME + login_ttl = 2626560 # 一个月的时间 @method_decorator(sensitive_post_parameters('password')) @method_decorator(csrf_protect) @@ -115,6 +122,8 @@ class LoginView(FormView): logger.info(self.redirect_field_name) auth.login(self.request, form.get_user()) + if self.request.POST.get("remember"): + self.request.session.set_expiry(self.login_ttl) return super(LoginView, self).form_valid(form) # return HttpResponseRedirect('/') else: @@ -126,8 +135,8 @@ class LoginView(FormView): redirect_to = self.request.POST.get(self.redirect_field_name) if not is_safe_url( - url=redirect_to, allowed_hosts=[ - self.request.get_host()]): + url=redirect_to, allowed_hosts=[ + self.request.get_host()]): redirect_to = self.success_url return redirect_to @@ -163,3 +172,32 @@ 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 = generate_code() + utils.send_verify_email(to_email, code) + utils.set_code(to_email, code) + + return HttpResponse("ok") diff --git a/blog/admin.py b/blog/admin.py index f5604bd..4b6968b 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -111,6 +111,7 @@ class TagAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'parent_category', 'index') exclude = ('slug', 'last_mod_time', 'created_time') diff --git a/blog/models.py b/blog/models.py index 6612599..067de72 100644 --- a/blog/models.py +++ b/blog/models.py @@ -174,9 +174,10 @@ class Category(BaseModel): null=True, on_delete=models.CASCADE) slug = models.SlugField(default='no-slug', max_length=60, blank=True) + index = models.IntegerField(default=0, verbose_name="权重排序-越大越靠前") class Meta: - ordering = ['name'] + ordering = ['-index'] verbose_name = "分类" verbose_name_plural = verbose_name 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/blog/static/assets/img/checkmark.png b/blog/static/assets/img/checkmark.png new file mode 100644 index 0000000..4bd0eb3 Binary files /dev/null and b/blog/static/assets/img/checkmark.png differ diff --git a/servermanager/api/__init__.py b/servermanager/api/__init__.py index 990c2c0..8b13789 100644 --- a/servermanager/api/__init__.py +++ b/servermanager/api/__init__.py @@ -1,14 +1 @@ -#!/usr/bin/env python -# encoding: utf-8 - -""" -@version: ?? -@author: liangliangyy -@license: MIT Licence -@contact: liangliangyy@gmail.com -@site: https://www.lylinux.net/ -@software: PyCharm -@file: __init__.py.py -@time: 2017/8/27 上午11:40 -""" diff --git a/servermanager/api/blogapi.py b/servermanager/api/blogapi.py index 24b3fea..9ac252b 100644 --- a/servermanager/api/blogapi.py +++ b/servermanager/api/blogapi.py @@ -1,19 +1,6 @@ -#!/usr/bin/env python -# encoding: utf-8 +from haystack.query import SearchQuerySet - -""" -@version: ?? -@author: liangliangyy -@license: MIT Licence -@contact: liangliangyy@gmail.com -@site: https://www.lylinux.net/ -@software: PyCharm -@file: blogapi.py -@time: 2017/8/27 上午11:40 -""" -from blog.models import Article, Category, Tag -from haystack.query import EmptySearchQuerySet, SearchQuerySet +from blog.models import Article, Category class BlogApi(): diff --git a/servermanager/api/commonapi.py b/servermanager/api/commonapi.py index 7669dde..56892d4 100644 --- a/servermanager/api/commonapi.py +++ b/servermanager/api/commonapi.py @@ -1,30 +1,17 @@ -#!/usr/bin/env python -# encoding: utf-8 - - -""" -@version: ?? -@author: liangliangyy -@license: MIT Licence -@contact: liangliangyy@gmail.com -@site: https://www.lylinux.net/ -@software: PyCharm -@file: commonapi.py -@time: 2017/9/2 上午1:43 -""" -import requests import json import logging +import requests + logger = logging.getLogger(__name__) -class TuLing(): +class TuLing: def __init__(self): self.__key__ = '2f1446eb0321804291b0a1e217c25bb5' self.__appid__ = 137762 - def __build_req_url(self, content): + def _build_req_url(self, content): return 'http://www.tuling123.com/openapi/api?key=%s&info=%s&userid=%s' % ( self.__key__, content, self.__appid__) @@ -34,7 +21,7 @@ class TuLing(): def getdata(self, content): try: - requrl = self.__build_req_url(content) + requrl = self._build_req_url(content) res = self.UserAgent(requrl).decode('utf-8') jsons = json.loads(res, encoding='utf-8') 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 f758b7a..1773896 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -25,7 +25,7 @@
{% comment %}Need help?{% endcomment %}
{% load oauth_tags %} @@ -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