From 1cf56263767ce23622b4d86de903a4ef16bef0ae Mon Sep 17 00:00:00 2001 From: hyt <691385292@qq.com> Date: Sun, 9 Nov 2025 00:47:58 +0800 Subject: [PATCH 1/3] =?UTF-8?q?hyt=5Faccounts=20APP=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/accounts/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 152 bytes .../__pycache__/admin.cpython-312.pyc | Bin 0 -> 3173 bytes src/accounts/__pycache__/apps.cpython-312.pyc | Bin 0 -> 401 bytes .../__pycache__/forms.cpython-312.pyc | Bin 0 -> 5825 bytes .../__pycache__/models.cpython-312.pyc | Bin 0 -> 2221 bytes src/accounts/__pycache__/urls.cpython-312.pyc | Bin 0 -> 1293 bytes .../user_login_backend.cpython-312.pyc | Bin 0 -> 1512 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 2196 bytes .../__pycache__/views.cpython-312.pyc | Bin 0 -> 10721 bytes src/accounts/admin.py | 113 +++++++ src/accounts/apps.py | 20 ++ src/accounts/forms.py | 210 ++++++++++++ src/accounts/migrations/0001_initial.py | 104 ++++++ ...s_remove_bloguser_created_time_and_more.py | 81 +++++ src/accounts/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-312.pyc | Bin 0 -> 4185 bytes ...user_created_time_and_more.cpython-312.pyc | Bin 0 -> 1901 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 163 bytes src/accounts/models.py | 81 +++++ src/accounts/templatetags/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 165 bytes src/accounts/tests.py | 296 ++++++++++++++++ src/accounts/urls.py | 50 +++ src/accounts/user_login_backend.py | 67 ++++ src/accounts/utils.py | 85 +++++ src/accounts/views.py | 316 ++++++++++++++++++ 27 files changed, 1423 insertions(+) create mode 100644 src/accounts/__init__.py create mode 100644 src/accounts/__pycache__/__init__.cpython-312.pyc create mode 100644 src/accounts/__pycache__/admin.cpython-312.pyc create mode 100644 src/accounts/__pycache__/apps.cpython-312.pyc create mode 100644 src/accounts/__pycache__/forms.cpython-312.pyc create mode 100644 src/accounts/__pycache__/models.cpython-312.pyc create mode 100644 src/accounts/__pycache__/urls.cpython-312.pyc create mode 100644 src/accounts/__pycache__/user_login_backend.cpython-312.pyc create mode 100644 src/accounts/__pycache__/utils.cpython-312.pyc create mode 100644 src/accounts/__pycache__/views.cpython-312.pyc create mode 100644 src/accounts/admin.py create mode 100644 src/accounts/apps.py create mode 100644 src/accounts/forms.py create mode 100644 src/accounts/migrations/0001_initial.py create mode 100644 src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py create mode 100644 src/accounts/migrations/__init__.py create mode 100644 src/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc create mode 100644 src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-312.pyc create mode 100644 src/accounts/migrations/__pycache__/__init__.cpython-312.pyc create mode 100644 src/accounts/models.py create mode 100644 src/accounts/templatetags/__init__.py create mode 100644 src/accounts/templatetags/__pycache__/__init__.cpython-312.pyc create mode 100644 src/accounts/tests.py create mode 100644 src/accounts/urls.py create mode 100644 src/accounts/user_login_backend.py create mode 100644 src/accounts/utils.py create mode 100644 src/accounts/views.py diff --git a/src/accounts/__init__.py b/src/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/__pycache__/__init__.cpython-312.pyc b/src/accounts/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8d19b85463826bc89669a9881a3c5696fadb01d GIT binary patch literal 152 zcmX@j%ge<81ceMYGC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!GIz0xDb7zTiSei` zQ3xo?&q_@$i775hj&aFK%uCOA%E?cUNlZ@8FU>0{j){-Y%*!l^kJl@xyv1RYo1ape ZlWJGQ3N(iih>JmtkIamWj77{q76A6kB-Q`` literal 0 HcmV?d00001 diff --git a/src/accounts/__pycache__/admin.cpython-312.pyc b/src/accounts/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e9919a87f776616434d5459fce62e987154cd3f GIT binary patch literal 3173 zcmb7GO>7&-6`t8YF8@qfVJ%9IUADGtubnEbqqt4spC*yN1G;u%}M#0TGq@`B9WJgqpuJr?b2!~S#0NjUygY*FElft znRV5^^nj4&K$>^uzO>LhU=%*Xi3a8Be86fUFzFwKE_>0bd3ER@^uze-AXhU zvdY|46@U@o9_H*yC1}*6aLz{LOe{9Jg4=?%-Jw!j?H)Y5sxd?{8v{~F zP6|9hh#;1?J_d4|v`7{H`&+e#_TBOwn7mE@k6BUzinpZ}W#cW09c$5+bo1M=xua16 zSW?w?uNCtVx=b{u53R)7&%p)&Lz(62B*fp^Vb3ldN~B@g3Ci^#D%b3&a>G!0uHw6P z-E}O-j%+RiRv4a^7hbb{4`hI?D~ttU{+ zkzYX+913aP!wE!6s`ew3XcENAfG9aK`XZT22}F6sQCgCPtBMBJ2H@#_Zxm)=B-Pog zkWyMDsMWkkIGm>i)I%?FA#7^cSacc0!~UtubDEZVPA(X8%LSa zfC9j>67; zb#eW~rN5{f)30}?Uw<$(w>$e00TW3ME z_d^!C+b`rIZNa%M>OuOvPk&7*X~HXG){xjq z(8e3dt9y}U^%8iN}FM=#wu zzNQ;@Pps>PID?_>cN!jwp=tIkl5Yap4NrsxLxTAlqWb`zk0n{1d_;ge&QSIALjojr z5o+#E@V1z2w33QCjv0e1Hrj6cOc za3Qg>vNJ&&D|h*r;>~-Rc{?-vZrekUv4G9vgUp{w^fBm2eJaTs7%+?>K@Sl$xB;fE zfaxK=>205cNIhBSE$vTzccro1kY#|=K-2Z}1jq4waxnH;d literal 0 HcmV?d00001 diff --git a/src/accounts/__pycache__/forms.cpython-312.pyc b/src/accounts/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67a1db99d16ff4d90889243f285fa817fc64fe76 GIT binary patch literal 5825 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 P_>eTxSp18?Bgpz6s+JzJ literal 0 HcmV?d00001 diff --git a/src/accounts/__pycache__/urls.cpython-312.pyc b/src/accounts/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d616a53a7b0864814d5d5a168ab35ce381e990e2 GIT binary patch literal 1293 zcmb7C&rcIU6rSymZK0GFS{N{mNFYFswy88B8iEFqcpyqVcxW1$&F<14?sn_URzfwR z{{f!(8%#NPduAu8a$?Tk&Xa-IMWf?A5%wm=OVs8J`QO zF2$#MO5v{P`Cf{g<1SMC`+aq|jV+~L38-RG*dM4vXKZn`AgD##@x|{y)du%Nb$7dh z>x4mP^rAu=|4FCbp(#U`Gz4WBG{&xKD3MD%ml6fuwX3|p-Zj#rF$x;7|J1O@iRO6ukrR=sWN>x*r z$P{R>cR+u_=h!U>!Xb(sqKO|U0^n<4YVPDI9f!&$Qy zo^9czg_BL3vSZVXFvAGZM)al~9dAYFtmvFQKFJQIcx40M;-##pg%>Qm(8PD`*los0 jb{HwnNLx7F!iyGOY~m$5c84+2$CHuJ(E9+H1jD}pWqwRk literal 0 HcmV?d00001 diff --git a/src/accounts/__pycache__/user_login_backend.cpython-312.pyc b/src/accounts/__pycache__/user_login_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..216a07ff6d193c4cd1323b2637edc75a62614ea5 GIT binary patch literal 1512 zcmaJ>UuYaf82@H=_x>c8Y-LIAq*gCYDOnOPAShKrYeHIm@f3;=Jr0)5?l!s0-EA_n zXL98VwM0oD)I3z9Z+Q?=BB4Ghlv4U8_>vGI=hA{;)7ZD@DU?3>&EB3I_3X#)@B3!v zn{Q^m?>F;PUtbTPEJ9;$N&@&zG}@wsM)M^!*1-mwl%Py`0p!Lr6}pDSG6gFm>Yh&pMt_%QWM#Q8B%6_L1aKv$S3o*#8*ijL)R zU&5;S{nngg%^M3Q=SwcL{lv8E@T}{dT`q7B3m|i+bkUD8XR+#Vj|npQv46cI^Ghb1 z<-RPI;Sn4Z|7Oigzr$RyVxBWAKV38{v+h*Moz0t;m&j-xG zo+>@6g%V`;R-Z*``1RUooNgn`+RwOVC34l zUkB40BijR`tLF}>5>GX>@$08|wc$N&bVnQA)5dqi_v($42&>Z#RohdK@2JN&PW`No zHIhfydJiQy^5UcW>akt*g*|m_M;-eUNIdn6mOPYDKjzQix}Ur<8HGF1o$FqnD6k`>wW{0oKX4q|KZ2-5Db)@`Q9@49((=r3>@A9593> za0=@apd#p$9T9erSh2Nqnhl`xW9?gi>BQ5D)Aky~Is6R56*#0)JatzadLs8XPHk)B zyV}HdV&c)=|4(v1#<)6s623}Kf^?f8=ro%}uEq>QP~9;69z(oD)snCihOt;ROW{b= zFl^T{3?{b2o)r*00cb#QEGBXmfJk{z(G~cEQYF@m^(d)kN>)-#yj_7$R#$w@4mh2$ z+=|BvGXWc0SM9kjBbkumoCzwlRuemD#cU;ZqP6+cYzPO$%fOQe$T33hLFOL3{8wav Nq&CL>22{bse*js6Y*hdN literal 0 HcmV?d00001 diff --git a/src/accounts/__pycache__/utils.cpython-312.pyc b/src/accounts/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..653df06632a0f66127a8694fab9b24bcfc9ba980 GIT binary patch literal 2196 zcmah~Yitx%6uvY2>~{O4yxOXhkP2&R1>+-a2%?}7V_IX8#7&dUuyfl^ncZ3M+y%E; z%oZD3w9qwHSfJry0Fjz5{7{~X|NR3Zk&KdP>}+>`v84R+m*?KuZd+pXCUft3-E+^m z=euWqs;crK7$2Z}{RdnK{l%F1;VBVwIUtT81u24tf@~EvAtDAvfzb}l5pf2coOWui zh&$-!vLE@z!@7b^j}hNFfWtKGU}u*G^T7DI%`hE7v~*zziZFj!ikK8sVbG1x zUI#7jO`<*_DJC6LC5eC%j({`%5xB9&KP#k$3L^*e(?3|*{j;Zr3Kz!m*&!f&5{y?# zpV7f7#+8EXkaCC4TH{9w`=)27FO{{Wm^&np&s_g*=F0WLmzlDbnscb)6!U<)!`99O2BU>YaUu*ig^!RpMB(gDO`q$JT)2{n5~K!mXC zWtQ{;QWlWZjS^zO5w8bxyJqaBbD6A_K9`$3ko#hk7j`K;ct9OeK6^QLY2Rbu__y-E z%OhWamvwGm?!+XxFH9WGXS3GDOZl4<*6|ZIYf!?^>|{VH!zzFCXklb*_SkS~&za%b z^!}ME2dy&~tn?_*4{nTE*Cz7WvxSk7*@^Q+Rt=o{rKtz3Vn zX2UJ-hN1&`8%aHwE~$RGPjrH2arFllV+3?;BvDeJ4BmVUK<9XTEb1CUK#a3@0?lIf z`)QVAC%hcC+jPL=H7j^(Axfd485kaqVg$s6{0(;ge9B(fU|-V{AbkctgJqvWGtP>U zZA05eaeDh?`>#!{x0_n;I-j-EEVn$cEQ7oinEy|6sBum1NMQdN^-kVP$EYSjrTYX9m0x1s+ogY+i)kemo{$9yB29;ItM%q z-cC$!SBy1CS>re&>|S}?Un3r$4K?q^h_1vnyosy>JxkNbz*KYyg76!vx{sc;(3<C^Xxm=FA%>yN-69-*UTo{Ugvd H^T_`J;J{~f literal 0 HcmV?d00001 diff --git a/src/accounts/__pycache__/views.cpython-312.pyc b/src/accounts/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9aa82693bb5636a97aee65332d00ea9135a994b GIT binary patch literal 10721 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 literal 0 HcmV?d00001 diff --git a/src/accounts/admin.py b/src/accounts/admin.py new file mode 100644 index 0000000..d2ef475 --- /dev/null +++ b/src/accounts/admin.py @@ -0,0 +1,113 @@ +#hyt: + +from django import forms +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserChangeForm +from django.contrib.auth.forms import UsernameField +from django.utils.translation import gettext_lazy as _ + +# Register your models here. +from .models import BlogUser + + +class BlogUserCreationForm(forms.ModelForm): + """ + 博客用户创建表单 + + 用于管理员后台创建新用户,提供密码验证和哈希处理功能 + 扩展了 Django 原生用户创建逻辑,支持自定义字段和来源标记 + """ + + password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) + + class Meta: + model = BlogUser + fields = ('email',) # 创建用户时只要求填写邮箱字段 + + def clean_password2(self): + """ + 密码确认验证 + + 检查两次输入的密码是否一致 + 确保用户在创建账户时正确确认密码 + + 返回: + 验证通过的密码 + + 异常: + ValidationError: 当两次密码不匹配时抛出 + """ + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError(_("passwords do not match")) + return password2 + + def save(self, commit=True): + """ + 保存用户信息 + + 重写保存方法,对密码进行哈希处理并标记用户来源 + + 参数: + commit: 是否立即保存到数据库 + + 返回: + 保存后的用户对象 + """ + # 保存提供的密码为哈希格式 + user = super().save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.source = 'adminsite' # 标记用户来源为管理员后台 + user.save() + return user + + +class BlogUserChangeForm(UserChangeForm): + """ + 博客用户信息修改表单 + + 用于管理员后台编辑现有用户信息 + 继承 Django 原生用户修改表单,支持所有字段编辑 + """ + + class Meta: + model = BlogUser + fields = '__all__' # 包含所有模型字段 + field_classes = {'username': UsernameField} # 用户名字段使用特定字段类 + + def __init__(self, *args, **kwargs): + """ + 初始化表单 + + 调用父类初始化方法,可在此处添加自定义初始化逻辑 + """ + super().__init__(*args, **kwargs) + + +class BlogUserAdmin(UserAdmin): + """ + 博客用户管理后台配置 + + 自定义用户模型在 Django Admin 后台的显示和编辑配置 + 扩展了默认的用户管理功能,添加了自定义字段显示 + """ + + form = BlogUserChangeForm # 用户编辑表单 + add_form = BlogUserCreationForm # 用户创建表单 + + # 列表页面显示的字段 + list_display = ( + 'id', + 'nickname', + 'username', + 'email', + 'last_login', + 'date_joined', + 'source' + ) + + list_display_links = ('id', 'username') # 可点击链接的字段 + ordering = ('-id',) # 按 ID 降序排列 \ No newline at end of file diff --git a/src/accounts/apps.py b/src/accounts/apps.py new file mode 100644 index 0000000..2148ee0 --- /dev/null +++ b/src/accounts/apps.py @@ -0,0 +1,20 @@ +#hyt: + +from django.apps import AppConfig + +class AccountsConfig(AppConfig): + """ + 账户管理应用配置类 + + 负责配置 accounts 应用在 Django 项目中的行为 + 包括应用名称、初始化逻辑、信号注册等配置项 + + 功能特性: + - 用户认证和权限管理 + - 用户资料管理 + - 登录注册功能 + - 会话管理 + """ + + # 应用路径标识 - Django 用于识别应用的完整 Python 路径 + name = 'accounts' \ No newline at end of file diff --git a/src/accounts/forms.py b/src/accounts/forms.py new file mode 100644 index 0000000..f68134e --- /dev/null +++ b/src/accounts/forms.py @@ -0,0 +1,210 @@ +#hyt: + +from django import forms +from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.core.exceptions import ValidationError +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ +from . import utils +from .models import BlogUser + + +class LoginForm(AuthenticationForm): + """ + 用户登录表单 + + 继承 Django 原生认证表单,自定义界面样式和占位符文本 + 提供用户名和密码的登录验证功能 + """ + + def __init__(self, *args, **kwargs): + """ + 初始化表单控件 + + 设置表单字段的样式类和占位符文本,优化用户体验 + """ + super(LoginForm, self).__init__(*args, **kwargs) + # 设置用户名字段的输入框样式 + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + # 设置密码字段的输入框样式 + self.fields['password'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + + +class RegisterForm(UserCreationForm): + """ + 用户注册表单 + + 处理新用户注册流程,包括用户名、邮箱验证和密码确认 + 扩展了 Django 原生用户创建表单,添加邮箱字段和样式定制 + """ + + def __init__(self, *args, **kwargs): + """ + 初始化注册表单控件 + + 为所有表单字段设置统一的样式类和占位符提示 + """ + super(RegisterForm, self).__init__(*args, **kwargs) + + # 设置各字段的输入框样式和占位符 + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + self.fields['email'].widget = widgets.EmailInput( + attrs={'placeholder': "email", "class": "form-control"}) + self.fields['password1'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + self.fields['password2'].widget = widgets.PasswordInput( + attrs={'placeholder': "repeat password", "class": "form-control"}) + + def clean_email(self): + """ + 邮箱唯一性验证 + + 检查邮箱是否已被其他用户注册,确保邮箱地址的唯一性 + + 返回: + 验证通过的邮箱地址 + + 异常: + ValidationError: 当邮箱已存在时抛出 + """ + email = self.cleaned_data['email'] + if get_user_model().objects.filter(email=email).exists(): + raise ValidationError(_("email already exists")) + return email + + class Meta: + """表单元数据配置""" + model = get_user_model() # 使用当前激活的用户模型 + fields = ("username", "email") # 注册时需要填写的字段 + + +class ForgetPasswordForm(forms.Form): + """ + 密码重置表单 + + 处理用户忘记密码时的重置流程,包含邮箱验证、验证码校验和新密码设置 + 通过多步骤验证确保账户安全 + """ + + # 新密码字段 - 第一次输入 + new_password1 = forms.CharField( + label=_("New password"), + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("New password") + } + ), + ) + + # 新密码字段 - 确认输入 + new_password2 = forms.CharField( + label="确认密码", + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("Confirm password") + } + ), + ) + + # 邮箱字段 - 用于身份验证 + email = forms.EmailField( + label='邮箱', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Email") + } + ), + ) + + # 验证码字段 - 安全验证 + code = forms.CharField( + label=_('Code'), + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Code") + } + ), + ) + + def clean_new_password2(self): + """ + 新密码确认验证 + + 检查两次输入的新密码是否一致,并验证密码强度 + + 返回: + 验证通过的密码 + + 异常: + ValidationError: 当密码不匹配或强度不足时抛出 + """ + password1 = self.data.get("new_password1") + password2 = self.data.get("new_password2") + # 检查密码是否匹配 + if password1 and password2 and password1 != password2: + raise ValidationError(_("passwords do not match")) + # 验证密码强度 + password_validation.validate_password(password2) + + return password2 + + def clean_email(self): + """ + 邮箱存在性验证 + + 验证输入的邮箱是否在系统中已注册 + + 返回: + 验证通过的邮箱地址 + + 异常: + ValidationError: 当邮箱不存在时抛出 + """ + user_email = self.cleaned_data.get("email") + if not BlogUser.objects.filter(email=user_email).exists(): + # TODO: 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + raise ValidationError(_("email does not exist")) + return user_email + + def clean_code(self): + """ + 验证码校验 + + 验证邮箱和验证码的匹配关系,确保重置请求的合法性 + + 返回: + 验证通过的验证码 + + 异常: + ValidationError: 当验证码无效时抛出 + """ + 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=_('Email'), + ) \ No newline at end of file diff --git a/src/accounts/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..e13c533 --- /dev/null +++ b/src/accounts/migrations/0001_initial.py @@ -0,0 +1,104 @@ +#hyt: +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + """ + BlogUser 模型的初始迁移文件 + + 创建自定义用户模型 BlogUser,扩展 Django 内置 User 模型 + 添加了昵称、时间戳和来源字段,支持中文显示和自定义排序 + """ + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='BlogUser', + fields=[ + # 主键字段 - 使用 BigAutoField 作为自增主键 + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + + # 认证相关字段 - Django 内置用户认证系统必需字段 + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + + # 权限相关字段 - 用户权限和状态管理 + ('is_superuser', models.BooleanField( + default=False, + help_text='Designates that this user has all permissions without explicitly assigning them.', + verbose_name='superuser status' + )), + + # 用户基本信息字段 - 用户名、姓名、邮箱等 + ('username', models.CharField( + error_messages={'unique': 'A user with that username already exists.'}, + help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', + max_length=150, + unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name='username' + )), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + + # 状态字段 - 用户状态标识 + ('is_staff', models.BooleanField( + default=False, + help_text='Designates whether the user can log into this admin site.', + verbose_name='staff status' + )), + ('is_active', models.BooleanField( + default=True, + help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', + verbose_name='active' + )), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + + # 自定义扩展字段 - 博客用户特有字段 + ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + + # 权限关联字段 - 用户组和权限的多对多关系 + ('groups', models.ManyToManyField( + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', + related_name='user_set', + related_query_name='user', + to='auth.group', + verbose_name='groups' + )), + ('user_permissions', models.ManyToManyField( + blank=True, + help_text='Specific permissions for this user.', + related_name='user_set', + related_query_name='user', + to='auth.permission', + verbose_name='user permissions' + )), + ], + options={ + # 模型元选项 - 定义模型在 admin 中的显示和排序 + 'verbose_name': '用户', # 单数显示名称 + 'verbose_name_plural': '用户', # 复数显示名称 + 'ordering': ['-id'], # 按 ID 降序排列 + 'get_latest_by': 'id', # 获取最新记录的依据字段 + }, + managers=[ + # 模型管理器 - 使用 Django 默认的用户管理器 + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] \ No newline at end of file diff --git a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py new file mode 100644 index 0000000..22eb985 --- /dev/null +++ b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -0,0 +1,81 @@ +#hyt: +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + """ + BlogUser 模型结构调整迁移文件 + + 对 accounts 应用的 BlogUser 模型进行字段优化和国际化改进: + 1. 调整时间字段命名,统一使用英文命名规范 + 2. 更新模型选项,改进 Admin 后台显示 + 3. 字段标签国际化,为多语言支持做准备 + """ + + dependencies = [ + ('accounts', '0001_initial'), # 依赖于初始迁移文件 + ] + + operations = [ + # 模型选项调整 - 更新 Admin 后台显示配置 + migrations.AlterModelOptions( + name='bloguser', + options={ + 'get_latest_by': 'id', # 按 ID 获取最新记录 + 'ordering': ['-id'], # 按 ID 降序排列 + 'verbose_name': 'user', # 单数显示名称(英文) + 'verbose_name_plural': 'user', # 复数显示名称(英文) + }, + ), + + # 字段清理 - 移除旧的时间字段 + migrations.RemoveField( + model_name='bloguser', + name='created_time', # 移除旧的创建时间字段 + ), + migrations.RemoveField( + model_name='bloguser', + name='last_mod_time', # 移除旧的修改时间字段 + ), + + # 字段添加 - 新增标准化时间字段 + migrations.AddField( + model_name='bloguser', + name='creation_time', + field=models.DateTimeField( + default=django.utils.timezone.now, # 默认值为当前时间 + verbose_name='creation time' # 字段显示名称(英文) + ), + ), + migrations.AddField( + model_name='bloguser', + name='last_modify_time', + field=models.DateTimeField( + default=django.utils.timezone.now, # 默认值为当前时间 + verbose_name='last modify time' # 字段显示名称(英文) + ), + ), + + # 字段调整 - 更新字段标签为英文 + migrations.AlterField( + model_name='bloguser', + name='nickname', + field=models.CharField( + blank=True, # 允许为空 + max_length=100, # 最大长度100字符 + verbose_name='nick name' # 字段显示名称(英文) + ), + ), + migrations.AlterField( + model_name='bloguser', + name='source', + field=models.CharField( + blank=True, # 允许为空 + max_length=100, # 最大长度100字符 + verbose_name='create source' # 字段显示名称(英文) + ), + ), + ] \ No newline at end of file diff --git a/src/accounts/migrations/__init__.py b/src/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc b/src/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95a7532cf8a7c71c7336beca79b4a8f47a6becac GIT binary patch literal 4185 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 literal 0 HcmV?d00001 diff --git a/src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-312.pyc b/src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82e80cab8388b555deb6a21f3461e464f5982b7c GIT binary patch literal 1901 zcmcIkOK;mo5MDk+QH+#Gw4F!o*oxz-V1QUoPHj;XO)I2E+ayg8AcY`+ptviV3PmbQ z%BkEOS|EoWbL*u)AVCE5>Yq@c7ZWfbVSDT;Hvx7LAgAswMaQ+}wwIE?a%Ohsn+Lz$ z-wFi{(B}~tZR8aI{$NV_WG2GiO)C5ZAb^4gIznCOh;>n5y68!DSp=U0C_M#GMnapT zr2xtIrIdW@nZ9UbUNOZGiPmu)Y(;M1heYWF5P9KkJ8?|bGZSU+6Om;{pe{gA7op$? zbx8zBo|X%WP=c}}zzltrpXEXJC5>hekH{zT;gv?HgWP}c(vEnYK8%vc6zdetqqg2#vJr|ET^`Jd`2o><4il&VEo;&m)e+0)?+oE^-i zy&UOkI@;NPL_0SP&B@k_{qx2JlDm}>j0;d-u zOhgxMNmpG+WTqN2DL5!HJu5oaA~T0Iv+MP+<Qxgz^eq1& zJ+{`jriXkd+Tgi}5(YhNqe)4qoT#^r5>ZXwk7*LkWS#UpMCH6`KI&N>LuDF7UF1XL z+b#+*yC_KMh8H1xi_PQCWLl^0p-!-gZt_ml8xV2{%gnt{qjS7ZrOA)^kMzp3%22Pwdgb}%FHc@P8CGj?wKlA- z$JO%~~T$g|Fm^ktTHIo6k-&u(A-^~+uT zn-hF59QD1kt2Ywg4=(A-4`1Nbbd-idatSw9VUsg0;m_Q}$oaCG(gj!2IDC~!9HvSo vqJET6`D1*Ys`S@@aEUHsNf3nB3J{Fn!NR`so}fI_hQ*b*xbi2U3J3TLEs@s% literal 0 HcmV?d00001 diff --git a/src/accounts/migrations/__pycache__/__init__.cpython-312.pyc b/src/accounts/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7090758445eb4b6b54ee58e6698b8fd3b5b3705f GIT binary patch literal 163 zcmX@j%ge<81ceMYGC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&obXDb7zTiSei` zQ3xo?&q_@$i775hj&aFK%uCOA%E?cUNlZ@8FU>0{j>*kTFG?)Q%+D*1iI30B%PfhH k*DI*J#bJ}1pHiBWYFESxG?EdBi$RQ!%#4hTMa)1J00&zsdH?_b literal 0 HcmV?d00001 diff --git a/src/accounts/models.py b/src/accounts/models.py new file mode 100644 index 0000000..33769bd --- /dev/null +++ b/src/accounts/models.py @@ -0,0 +1,81 @@ +#hyt: + +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from djangoblog.utils import get_current_site + + +# Create your models here. + +class BlogUser(AbstractUser): + """ + 博客用户模型 + + 扩展 Django 内置 AbstractUser 模型,添加博客系统特有的用户字段 + 支持用户昵称、时间戳跟踪和来源标识等功能 + """ + + # 昵称字段 - 用户显示名称,可为空 + nickname = models.CharField(_('nick name'), max_length=100, blank=True) + + # 创建时间 - 用户账户创建的时间戳 + creation_time = models.DateTimeField(_('creation time'), default=now) + + # 最后修改时间 - 用户信息最后更新的时间戳 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + # 来源标识 - 用户注册来源(如网站、管理员创建等) + source = models.CharField(_('create source'), max_length=100, blank=True) + + def get_absolute_url(self): + """ + 获取用户详情页的绝对URL + + 用于生成用户个人主页的链接,支持反向解析 + + 返回: + 用户详情页的URL路径 + """ + return reverse( + 'blog:author_detail', kwargs={ + 'author_name': self.username}) + + def __str__(self): + """ + 对象字符串表示 + + 在Admin后台和Shell中显示用户的邮箱地址 + + 返回: + 用户的邮箱地址 + """ + return self.email + + def get_full_url(self): + """ + 获取用户的完整URL(包含域名) + + 生成包含协议和域名的完整用户主页链接 + + 返回: + 用户的完整主页URL + """ + site = get_current_site().domain + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + + class Meta: + """ + 模型元数据配置 + + 定义模型在数据库和Admin后台中的行为 + """ + + ordering = ['-id'] # 默认按ID降序排列 + verbose_name = _('user') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称(与单数相同) + get_latest_by = 'id' # 获取最新记录的依据字段 \ No newline at end of file diff --git a/src/accounts/templatetags/__init__.py b/src/accounts/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/templatetags/__pycache__/__init__.cpython-312.pyc b/src/accounts/templatetags/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3061f4f66f141d3535d5f7a22c5eaec4ba4edca4 GIT binary patch literal 165 zcmX@j%ge<81ceMYGC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&fVWDb7zTiSei` zQ3xo?&q_@$i775hj&aFK%uCOA%E?cUNlZ@8FU>0{jwwmaEyzhMNi9iCFOG?i&& typing.Optional[str]: + """ + 验证验证码是否有效 + + 检查用户输入的验证码与缓存中的验证码是否匹配 + 用于验证邮箱验证码的正确性 + + 参数: + email: 请求验证的邮箱地址 + code: 用户输入的验证码 + + 返回: + str: 验证失败时返回错误信息 + None: 验证成功时返回None + + 注意: + 这里的错误处理不太合理,应该采用raise抛出异常 + 否则调用方也需要对error进行处理,增加了调用复杂度 + """ + cache_code = get_code(email) + if cache_code != code: + return gettext("Verification code error") + + +def set_code(email: str, code: str): + """ + 设置验证码到缓存 + + 将验证码与邮箱关联并存储到缓存中,设置5分钟的有效期 + + 参数: + email: 邮箱地址,作为缓存的键 + code: 验证码,作为缓存的值 + """ + cache.set(email, code, _code_ttl.seconds) + + +def get_code(email: str) -> typing.Optional[str]: + """ + 从缓存获取验证码 + + 根据邮箱地址从缓存中获取对应的验证码 + 如果验证码不存在或已过期,返回None + + 参数: + email: 邮箱地址,作为缓存的键 + + 返回: + str: 找到的验证码 + None: 验证码不存在或已过期 + """ + return cache.get(email) diff --git a/src/accounts/views.py b/src/accounts/views.py new file mode 100644 index 0000000..87e1714 --- /dev/null +++ b/src/accounts/views.py @@ -0,0 +1,316 @@ +#hyt: + +import logging +from django.utils.translation import gettext_lazy as _ +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.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.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme +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, delete_sidebar_cache +from . import utils +from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm +from .models import BlogUser + +# 日志记录器 +logger = logging.getLogger(__name__) + + +# Create your views here. + +class RegisterView(FormView): + """ + 用户注册视图 + + 处理新用户注册流程,包括表单验证、用户创建和邮箱验证邮件发送 + 注册后用户处于未激活状态,需要邮箱验证后才能登录 + """ + form_class = RegisterForm # 注册表单类 + template_name = 'account/registration_form.html' # 注册页面模板 + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + """ + 请求分发方法 + + 添加CSRF保护装饰器,防止跨站请求伪造攻击 + """ + return super(RegisterView, self).dispatch(*args, **kwargs) + + def form_valid(self, form): + """ + 表单验证通过处理 + + 保存用户信息,发送邮箱验证邮件,跳转到结果页面 + """ + if form.is_valid(): + # 保存用户信息但不立即提交到数据库 + user = form.save(False) + user.is_active = False # 设置用户为未激活状态 + user.source = 'Register' # 标记用户来源为注册 + user.save(True) # 保存到数据库 + + # 生成邮箱验证链接 + site = get_current_site().domain + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + + # 开发环境下使用本地地址 + if settings.DEBUG: + site = '127.0.0.1:8000' + + path = reverse('account:result') + url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( + site=site, path=path, id=user.id, sign=sign) + + # 构建邮件内容 + content = """ +

请点击下面链接验证您的邮箱

+ + {url} + + 再次感谢您! +
+ 如果上面链接无法打开,请将此链接复制至浏览器。 + {url} + """.format(url=url) + # 发送验证邮件 + send_email( + emailto=[ + user.email, + ], + title='验证您的电子邮箱', + content=content) + + # 跳转到注册结果页面 + url = reverse('accounts:result') + \ + '?type=register&id=' + str(user.id) + return HttpResponseRedirect(url) + else: + # 表单验证失败,重新渲染表单页面 + return self.render_to_response({ + 'form': form + }) + + +class LogoutView(RedirectView): + """ + 用户退出登录视图 + + 处理用户退出登录流程,清理会话和缓存,重定向到登录页面 + """ + url = '/login/' # 退出后重定向的URL + + @method_decorator(never_cache) + def dispatch(self, request, *args, **kwargs): + """ + 请求分发方法 + + 添加不缓存装饰器,确保退出后页面不被缓存 + """ + return super(LogoutView, self).dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + """ + GET请求处理 + + 执行退出登录操作,清理用户会话和侧边栏缓存 + """ + logout(request) # Django内置退出登录方法 + delete_sidebar_cache() # 清理侧边栏缓存 + return super(LogoutView, self).get(request, *args, **kwargs) + + +class LoginView(FormView): + """ + 用户登录视图 + + 处理用户登录认证,支持记住登录状态和重定向功能 + """ + form_class = LoginForm # 登录表单类 + template_name = 'account/login.html' # 登录页面模板 + success_url = '/' # 登录成功默认跳转URL + redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名 + login_ttl = 2626560 # 记住登录状态的会话有效期(一个月) + + @method_decorator(sensitive_post_parameters('password')) # 保护密码参数 + @method_decorator(csrf_protect) # CSRF保护 + @method_decorator(never_cache) # 禁止缓存 + def dispatch(self, request, *args, **kwargs): + """ + 请求分发方法 + + 添加安全相关的装饰器,保护登录过程的安全性 + """ + return super(LoginView, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + """ + 获取模板上下文数据 + + 处理重定向参数,确保登录后能正确跳转 + """ + redirect_to = self.request.GET.get(self.redirect_field_name) + if redirect_to is None: + redirect_to = '/' # 默认重定向到首页 + kwargs['redirect_to'] = redirect_to + + return super(LoginView, self).get_context_data(**kwargs) + + def form_valid(self, form): + """ + 表单验证通过处理 + + 执行用户登录认证,处理记住登录状态选项 + """ + form = AuthenticationForm(data=self.request.POST, request=self.request) + + if form.is_valid(): + # 登录成功,清理缓存并记录日志 + delete_sidebar_cache() + 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) + else: + # 登录失败,重新显示表单 + return self.render_to_response({ + 'form': form + }) + + def get_success_url(self): + """ + 获取登录成功后的跳转URL + + 验证重定向URL的安全性,防止开放重定向攻击 + """ + redirect_to = self.request.POST.get(self.redirect_field_name) + # 验证重定向URL是否安全 + if not url_has_allowed_host_and_scheme( + url=redirect_to, allowed_hosts=[ + self.request.get_host()]): + redirect_to = self.success_url # 不安全则使用默认URL + return redirect_to + + +def account_result(request): + """ + 账户操作结果页面视图 + + 显示注册成功或邮箱验证成功的结果信息 + 处理邮箱验证链接的验证逻辑 + """ + type = request.GET.get('type') # 操作类型:register或validation + id = request.GET.get('id') # 用户ID + + user = get_object_or_404(get_user_model(), id=id) + logger.info(type) + + # 如果用户已激活,直接跳转到首页 + if user.is_active: + return HttpResponseRedirect('/') + + # 处理注册结果或邮箱验证 + if type and type in ['register', 'validation']: + if type == 'register': + # 注册成功页面 + content = ''' + 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 + ''' + title = '注册成功' + else: + # 邮箱验证处理 + c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + sign = request.GET.get('sign') + # 验证签名防止篡改 + if sign != c_sign: + return HttpResponseForbidden() + # 激活用户账户 + user.is_active = True + user.save() + content = ''' + 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 + ''' + title = '验证成功' + # 渲染结果页面 + return render(request, 'account/result.html', { + 'title': title, + 'content': content + }) + 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): + """ + POST请求处理 + + 验证邮箱地址并发送密码重置验证码 + """ + 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") # 发送成功 \ No newline at end of file -- 2.34.1 From 009e363737b4128245e676db0ac9a41590fa61ca Mon Sep 17 00:00:00 2001 From: hyt <691385292@qq.com> Date: Sun, 9 Nov 2025 20:27:01 +0800 Subject: [PATCH 2/3] =?UTF-8?q?hyt=5Faccounts=20APP=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/accounts/admin.py | 76 ++----------- src/accounts/apps.py | 19 +--- src/accounts/forms.py | 107 ++---------------- src/accounts/models.py | 54 +-------- src/accounts/urls.py | 62 ++++------- src/accounts/user_login_backend.py | 53 +-------- src/accounts/utils.py | 68 +++--------- src/accounts/views.py | 170 +++++------------------------ 8 files changed, 95 insertions(+), 514 deletions(-) diff --git a/src/accounts/admin.py b/src/accounts/admin.py index d2ef475..32e483c 100644 --- a/src/accounts/admin.py +++ b/src/accounts/admin.py @@ -1,5 +1,3 @@ -#hyt: - from django import forms from django.contrib.auth.admin import UserAdmin from django.contrib.auth.forms import UserChangeForm @@ -11,33 +9,15 @@ from .models import BlogUser class BlogUserCreationForm(forms.ModelForm): - """ - 博客用户创建表单 - - 用于管理员后台创建新用户,提供密码验证和哈希处理功能 - 扩展了 Django 原生用户创建逻辑,支持自定义字段和来源标记 - """ - password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: model = BlogUser - fields = ('email',) # 创建用户时只要求填写邮箱字段 + fields = ('email',) def clean_password2(self): - """ - 密码确认验证 - - 检查两次输入的密码是否一致 - 确保用户在创建账户时正确确认密码 - - 返回: - 验证通过的密码 - - 异常: - ValidationError: 当两次密码不匹配时抛出 - """ + # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: @@ -45,60 +25,28 @@ class BlogUserCreationForm(forms.ModelForm): return password2 def save(self, commit=True): - """ - 保存用户信息 - - 重写保存方法,对密码进行哈希处理并标记用户来源 - - 参数: - commit: 是否立即保存到数据库 - - 返回: - 保存后的用户对象 - """ - # 保存提供的密码为哈希格式 + # Save the provided password in hashed format user = super().save(commit=False) user.set_password(self.cleaned_data["password1"]) if commit: - user.source = 'adminsite' # 标记用户来源为管理员后台 + user.source = 'adminsite' user.save() return user class BlogUserChangeForm(UserChangeForm): - """ - 博客用户信息修改表单 - - 用于管理员后台编辑现有用户信息 - 继承 Django 原生用户修改表单,支持所有字段编辑 - """ - class Meta: model = BlogUser - fields = '__all__' # 包含所有模型字段 - field_classes = {'username': UsernameField} # 用户名字段使用特定字段类 + fields = '__all__' + field_classes = {'username': UsernameField} def __init__(self, *args, **kwargs): - """ - 初始化表单 - - 调用父类初始化方法,可在此处添加自定义初始化逻辑 - """ super().__init__(*args, **kwargs) class BlogUserAdmin(UserAdmin): - """ - 博客用户管理后台配置 - - 自定义用户模型在 Django Admin 后台的显示和编辑配置 - 扩展了默认的用户管理功能,添加了自定义字段显示 - """ - - form = BlogUserChangeForm # 用户编辑表单 - add_form = BlogUserCreationForm # 用户创建表单 - - # 列表页面显示的字段 + form = BlogUserChangeForm + add_form = BlogUserCreationForm list_display = ( 'id', 'nickname', @@ -106,8 +54,6 @@ class BlogUserAdmin(UserAdmin): 'email', 'last_login', 'date_joined', - 'source' - ) - - list_display_links = ('id', 'username') # 可点击链接的字段 - ordering = ('-id',) # 按 ID 降序排列 \ No newline at end of file + 'source') + list_display_links = ('id', 'username') + ordering = ('-id',) diff --git a/src/accounts/apps.py b/src/accounts/apps.py index 2148ee0..9b3fc5a 100644 --- a/src/accounts/apps.py +++ b/src/accounts/apps.py @@ -1,20 +1,5 @@ -#hyt: - from django.apps import AppConfig -class AccountsConfig(AppConfig): - """ - 账户管理应用配置类 - - 负责配置 accounts 应用在 Django 项目中的行为 - 包括应用名称、初始化逻辑、信号注册等配置项 - 功能特性: - - 用户认证和权限管理 - - 用户资料管理 - - 登录注册功能 - - 会话管理 - """ - - # 应用路径标识 - Django 用于识别应用的完整 Python 路径 - name = 'accounts' \ No newline at end of file +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/src/accounts/forms.py b/src/accounts/forms.py index f68134e..fce4137 100644 --- a/src/accounts/forms.py +++ b/src/accounts/forms.py @@ -1,5 +1,3 @@ -#hyt: - from django import forms from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.forms import AuthenticationForm, UserCreationForm @@ -11,45 +9,18 @@ from .models import BlogUser class LoginForm(AuthenticationForm): - """ - 用户登录表单 - - 继承 Django 原生认证表单,自定义界面样式和占位符文本 - 提供用户名和密码的登录验证功能 - """ - def __init__(self, *args, **kwargs): - """ - 初始化表单控件 - - 设置表单字段的样式类和占位符文本,优化用户体验 - """ super(LoginForm, self).__init__(*args, **kwargs) - # 设置用户名字段的输入框样式 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) - # 设置密码字段的输入框样式 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) class RegisterForm(UserCreationForm): - """ - 用户注册表单 - - 处理新用户注册流程,包括用户名、邮箱验证和密码确认 - 扩展了 Django 原生用户创建表单,添加邮箱字段和样式定制 - """ - def __init__(self, *args, **kwargs): - """ - 初始化注册表单控件 - - 为所有表单字段设置统一的样式类和占位符提示 - """ super(RegisterForm, self).__init__(*args, **kwargs) - # 设置各字段的输入框样式和占位符 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -60,37 +31,17 @@ class RegisterForm(UserCreationForm): attrs={'placeholder': "repeat password", "class": "form-control"}) def clean_email(self): - """ - 邮箱唯一性验证 - - 检查邮箱是否已被其他用户注册,确保邮箱地址的唯一性 - - 返回: - 验证通过的邮箱地址 - - 异常: - ValidationError: 当邮箱已存在时抛出 - """ email = self.cleaned_data['email'] if get_user_model().objects.filter(email=email).exists(): raise ValidationError(_("email already exists")) return email class Meta: - """表单元数据配置""" - model = get_user_model() # 使用当前激活的用户模型 - fields = ("username", "email") # 注册时需要填写的字段 + model = get_user_model() + fields = ("username", "email") class ForgetPasswordForm(forms.Form): - """ - 密码重置表单 - - 处理用户忘记密码时的重置流程,包含邮箱验证、验证码校验和新密码设置 - 通过多步骤验证确保账户安全 - """ - - # 新密码字段 - 第一次输入 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -101,7 +52,6 @@ class ForgetPasswordForm(forms.Form): ), ) - # 新密码字段 - 确认输入 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -112,7 +62,6 @@ class ForgetPasswordForm(forms.Form): ), ) - # 邮箱字段 - 用于身份验证 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -123,7 +72,6 @@ class ForgetPasswordForm(forms.Form): ), ) - # 验证码字段 - 安全验证 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -135,57 +83,24 @@ class ForgetPasswordForm(forms.Form): ) def clean_new_password2(self): - """ - 新密码确认验证 - - 检查两次输入的新密码是否一致,并验证密码强度 - - 返回: - 验证通过的密码 - - 异常: - ValidationError: 当密码不匹配或强度不足时抛出 - """ password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") - # 检查密码是否匹配 if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) - # 验证密码强度 password_validation.validate_password(password2) return password2 def clean_email(self): - """ - 邮箱存在性验证 - - 验证输入的邮箱是否在系统中已注册 - - 返回: - 验证通过的邮箱地址 - - 异常: - ValidationError: 当邮箱不存在时抛出 - """ user_email = self.cleaned_data.get("email") - if not BlogUser.objects.filter(email=user_email).exists(): - # TODO: 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + if not BlogUser.objects.filter( + email=user_email + ).exists(): + # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 raise ValidationError(_("email does not exist")) return user_email def clean_code(self): - """ - 验证码校验 - - 验证邮箱和验证码的匹配关系,确保重置请求的合法性 - - 返回: - 验证通过的验证码 - - 异常: - ValidationError: 当验证码无效时抛出 - """ code = self.cleaned_data.get("code") error = utils.verify( email=self.cleaned_data.get("email"), @@ -197,14 +112,6 @@ class ForgetPasswordForm(forms.Form): class ForgetPasswordCodeForm(forms.Form): - """ - 密码重置验证码请求表单 - - 用于请求发送密码重置验证码,只需提供邮箱地址 - 是密码重置流程的第一步 - """ - - # 邮箱字段 - 用于发送验证码 email = forms.EmailField( label=_('Email'), - ) \ No newline at end of file + ) diff --git a/src/accounts/models.py b/src/accounts/models.py index 33769bd..3baddbb 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -1,5 +1,3 @@ -#hyt: - from django.contrib.auth.models import AbstractUser from django.db import models from django.urls import reverse @@ -11,71 +9,27 @@ from djangoblog.utils import get_current_site # Create your models here. class BlogUser(AbstractUser): - """ - 博客用户模型 - - 扩展 Django 内置 AbstractUser 模型,添加博客系统特有的用户字段 - 支持用户昵称、时间戳跟踪和来源标识等功能 - """ - - # 昵称字段 - 用户显示名称,可为空 nickname = models.CharField(_('nick name'), max_length=100, blank=True) - - # 创建时间 - 用户账户创建的时间戳 creation_time = models.DateTimeField(_('creation time'), default=now) - - # 最后修改时间 - 用户信息最后更新的时间戳 last_modify_time = models.DateTimeField(_('last modify time'), default=now) - - # 来源标识 - 用户注册来源(如网站、管理员创建等) source = models.CharField(_('create source'), max_length=100, blank=True) def get_absolute_url(self): - """ - 获取用户详情页的绝对URL - - 用于生成用户个人主页的链接,支持反向解析 - - 返回: - 用户详情页的URL路径 - """ return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) def __str__(self): - """ - 对象字符串表示 - - 在Admin后台和Shell中显示用户的邮箱地址 - - 返回: - 用户的邮箱地址 - """ return self.email def get_full_url(self): - """ - 获取用户的完整URL(包含域名) - - 生成包含协议和域名的完整用户主页链接 - - 返回: - 用户的完整主页URL - """ site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - """ - 模型元数据配置 - - 定义模型在数据库和Admin后台中的行为 - """ - - ordering = ['-id'] # 默认按ID降序排列 - verbose_name = _('user') # 单数显示名称 - verbose_name_plural = verbose_name # 复数显示名称(与单数相同) - get_latest_by = 'id' # 获取最新记录的依据字段 \ No newline at end of file + ordering = ['-id'] + verbose_name = _('user') + verbose_name_plural = verbose_name + get_latest_by = 'id' diff --git a/src/accounts/urls.py b/src/accounts/urls.py index d6c0d54..107a801 100644 --- a/src/accounts/urls.py +++ b/src/accounts/urls.py @@ -1,50 +1,28 @@ -#hyt: - from django.urls import path from django.urls import re_path from . import views from .forms import LoginForm -# 应用命名空间 - 用于URL反向解析时区分不同应用的URL app_name = "accounts" -# URL模式配置 - 定义账户管理相关的所有路由规则 -urlpatterns = [ - # 用户登录路由 - # 使用正则表达式匹配 /login/ 路径 - re_path(r'^login/$', - views.LoginView.as_view(success_url='/'), # 登录成功后跳转到首页 - name='login', # URL名称,用于反向解析 - kwargs={'authentication_form': LoginForm}), # 传入自定义登录表单 - - # 用户注册路由 - # 使用正则表达式匹配 /register/ 路径 - re_path(r'^register/$', - views.RegisterView.as_view(success_url="/"), # 注册成功后跳转到首页 - name='register'), # URL名称,用于反向解析 - - # 用户退出登录路由 - # 使用正则表达式匹配 /logout/ 路径 - re_path(r'^logout/$', - views.LogoutView.as_view(), # 使用Django内置的退出视图 - name='logout'), # URL名称,用于反向解析 - - # 账户操作结果页面路由 - # 使用path匹配固定路径 /account/result.html - path(r'account/result.html', - views.account_result, # 函数视图,显示账户操作结果 - name='result'), # URL名称,用于反向解析 - - # 忘记密码页面路由 - # 使用正则表达式匹配 /forget_password/ 路径 - re_path(r'^forget_password/$', - views.ForgetPasswordView.as_view(), # 类视图,处理密码重置 - name='forget_password'), # URL名称,用于反向解析 - - # 忘记密码验证码请求路由 - # 使用正则表达式匹配 /forget_password_code/ 路径 - re_path(r'^forget_password_code/$', - views.ForgetPasswordEmailCode.as_view(), # 类视图,发送验证码 - name='forget_password_code'), # URL名称,用于反向解析 -] \ No newline at end of file +urlpatterns = [re_path(r'^login/$', + views.LoginView.as_view(success_url='/'), + name='login', + kwargs={'authentication_form': LoginForm}), + re_path(r'^register/$', + views.RegisterView.as_view(success_url="/"), + name='register'), + re_path(r'^logout/$', + views.LogoutView.as_view(), + name='logout'), + path(r'account/result.html', + views.account_result, + name='result'), + re_path(r'^forget_password/$', + views.ForgetPasswordView.as_view(), + name='forget_password'), + re_path(r'^forget_password_code/$', + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), + ] diff --git a/src/accounts/user_login_backend.py b/src/accounts/user_login_backend.py index c851bd0..73cdca1 100644 --- a/src/accounts/user_login_backend.py +++ b/src/accounts/user_login_backend.py @@ -1,67 +1,26 @@ -#hyt: - from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend class EmailOrUsernameModelBackend(ModelBackend): """ - 自定义认证后端 - 支持邮箱或用户名登录 - - 扩展 Django 原生认证系统,允许用户使用用户名或邮箱地址进行登录 - 提供更灵活的用户认证方式,提升用户体验 + 允许使用用户名或邮箱登录 """ def authenticate(self, request, username=None, password=None, **kwargs): - """ - 用户认证方法 - - 根据输入的用户名判断是邮箱还是用户名,并进行相应的认证处理 - 支持两种登录方式: - - 用户名登录:username = "admin" - - 邮箱登录:username = "admin@example.com" - - 参数: - request: HttpRequest 对象 - username: 用户输入的用户名或邮箱地址 - password: 用户输入的密码 - **kwargs: 其他关键字参数 - - 返回: - User 对象: 认证成功时返回用户对象 - None: 认证失败时返回 None - """ - # 根据输入内容判断是邮箱还是用户名 if '@' in username: - kwargs = {'email': username} # 按邮箱查询 + kwargs = {'email': username} else: - kwargs = {'username': username} # 按用户名查询 - + kwargs = {'username': username} try: - # 根据查询条件获取用户对象 user = get_user_model().objects.get(**kwargs) - # 验证密码是否正确 if user.check_password(password): return user except get_user_model().DoesNotExist: - # 用户不存在时返回 None return None - def get_user(self, user_id): - """ - 根据用户ID获取用户对象 - - 用于会话认证期间从用户ID获取用户对象 - 保持与 Django 原生认证后端的兼容性 - - 参数: - user_id: 用户的主键ID - - 返回: - User 对象: 用户存在时返回用户对象 - None: 用户不存在时返回 None - """ + def get_user(self, username): try: - return get_user_model().objects.get(pk=user_id) + return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: - return None \ No newline at end of file + return None diff --git a/src/accounts/utils.py b/src/accounts/utils.py index 265471f..4b94bdf 100644 --- a/src/accounts/utils.py +++ b/src/accounts/utils.py @@ -1,5 +1,3 @@ -#hyt: - import typing from datetime import timedelta @@ -9,21 +7,15 @@ from django.utils.translation import gettext_lazy as _ from djangoblog.utils import send_email -# 验证码有效期配置 - 5分钟过期时间 _code_ttl = timedelta(minutes=5) def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): - """ - 发送邮箱验证邮件 - - 用于密码重置、邮箱验证等场景,向指定邮箱发送包含验证码的邮件 - 邮件内容支持国际化,验证码有效期为5分钟 - - 参数: - to_mail: 接收邮件的邮箱地址 - code: 需要发送的验证码 - subject: 邮件主题,默认为"Verify Email" + """发送重设密码验证码 + Args: + to_mail: 接受邮箱 + subject: 邮件主题 + code: 验证码 """ html_content = _( "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " @@ -32,23 +24,15 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")) def verify(email: str, code: str) -> typing.Optional[str]: - """ - 验证验证码是否有效 - - 检查用户输入的验证码与缓存中的验证码是否匹配 - 用于验证邮箱验证码的正确性 - - 参数: - email: 请求验证的邮箱地址 - code: 用户输入的验证码 - - 返回: - str: 验证失败时返回错误信息 - None: 验证成功时返回None - - 注意: - 这里的错误处理不太合理,应该采用raise抛出异常 - 否则调用方也需要对error进行处理,增加了调用复杂度 + """验证code是否有效 + Args: + email: 请求邮箱 + code: 验证码 + Return: + 如果有错误就返回错误str + Node: + 这里的错误处理不太合理,应该采用raise抛出 + 否测调用方也需要对error进行处理 """ cache_code = get_code(email) if cache_code != code: @@ -56,30 +40,10 @@ def verify(email: str, code: str) -> typing.Optional[str]: def set_code(email: str, code: str): - """ - 设置验证码到缓存 - - 将验证码与邮箱关联并存储到缓存中,设置5分钟的有效期 - - 参数: - email: 邮箱地址,作为缓存的键 - code: 验证码,作为缓存的值 - """ + """设置code""" cache.set(email, code, _code_ttl.seconds) def get_code(email: str) -> typing.Optional[str]: - """ - 从缓存获取验证码 - - 根据邮箱地址从缓存中获取对应的验证码 - 如果验证码不存在或已过期,返回None - - 参数: - email: 邮箱地址,作为缓存的键 - - 返回: - str: 找到的验证码 - None: 验证码不存在或已过期 - """ + """获取code""" return cache.get(email) diff --git a/src/accounts/views.py b/src/accounts/views.py index 87e1714..ae67aec 100644 --- a/src/accounts/views.py +++ b/src/accounts/views.py @@ -1,5 +1,3 @@ -#hyt: - import logging from django.utils.translation import gettext_lazy as _ from django.conf import settings @@ -28,57 +26,34 @@ from . import utils from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm from .models import BlogUser -# 日志记录器 logger = logging.getLogger(__name__) # Create your views here. class RegisterView(FormView): - """ - 用户注册视图 - - 处理新用户注册流程,包括表单验证、用户创建和邮箱验证邮件发送 - 注册后用户处于未激活状态,需要邮箱验证后才能登录 - """ - form_class = RegisterForm # 注册表单类 - template_name = 'account/registration_form.html' # 注册页面模板 + form_class = RegisterForm + template_name = 'account/registration_form.html' @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): - """ - 请求分发方法 - - 添加CSRF保护装饰器,防止跨站请求伪造攻击 - """ return super(RegisterView, self).dispatch(*args, **kwargs) def form_valid(self, form): - """ - 表单验证通过处理 - - 保存用户信息,发送邮箱验证邮件,跳转到结果页面 - """ if form.is_valid(): - # 保存用户信息但不立即提交到数据库 user = form.save(False) - user.is_active = False # 设置用户为未激活状态 - user.source = 'Register' # 标记用户来源为注册 - user.save(True) # 保存到数据库 - - # 生成邮箱验证链接 + user.is_active = False + user.source = 'Register' + user.save(True) site = get_current_site().domain sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) - # 开发环境下使用本地地址 if settings.DEBUG: site = '127.0.0.1:8000' - path = reverse('account:result') url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) - # 构建邮件内容 content = """

请点击下面链接验证您的邮箱

@@ -89,7 +64,6 @@ class RegisterView(FormView): 如果上面链接无法打开,请将此链接复制至浏览器。 {url} """.format(url=url) - # 发送验证邮件 send_email( emailto=[ user.email, @@ -97,220 +71,134 @@ class RegisterView(FormView): title='验证您的电子邮箱', content=content) - # 跳转到注册结果页面 url = reverse('accounts:result') + \ '?type=register&id=' + str(user.id) return HttpResponseRedirect(url) else: - # 表单验证失败,重新渲染表单页面 return self.render_to_response({ 'form': form }) class LogoutView(RedirectView): - """ - 用户退出登录视图 - - 处理用户退出登录流程,清理会话和缓存,重定向到登录页面 - """ - url = '/login/' # 退出后重定向的URL + url = '/login/' @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - """ - 请求分发方法 - - 添加不缓存装饰器,确保退出后页面不被缓存 - """ return super(LogoutView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): - """ - GET请求处理 - - 执行退出登录操作,清理用户会话和侧边栏缓存 - """ - logout(request) # Django内置退出登录方法 - delete_sidebar_cache() # 清理侧边栏缓存 + logout(request) + delete_sidebar_cache() return super(LogoutView, self).get(request, *args, **kwargs) class LoginView(FormView): - """ - 用户登录视图 - - 处理用户登录认证,支持记住登录状态和重定向功能 - """ - form_class = LoginForm # 登录表单类 - template_name = 'account/login.html' # 登录页面模板 - success_url = '/' # 登录成功默认跳转URL - redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名 - login_ttl = 2626560 # 记住登录状态的会话有效期(一个月) - - @method_decorator(sensitive_post_parameters('password')) # 保护密码参数 - @method_decorator(csrf_protect) # CSRF保护 - @method_decorator(never_cache) # 禁止缓存 + form_class = LoginForm + 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) + @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - """ - 请求分发方法 - 添加安全相关的装饰器,保护登录过程的安全性 - """ return super(LoginView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): - """ - 获取模板上下文数据 - - 处理重定向参数,确保登录后能正确跳转 - """ redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: - redirect_to = '/' # 默认重定向到首页 + redirect_to = '/' kwargs['redirect_to'] = redirect_to return super(LoginView, self).get_context_data(**kwargs) def form_valid(self, form): - """ - 表单验证通过处理 - - 执行用户登录认证,处理记住登录状态选项 - """ form = AuthenticationForm(data=self.request.POST, request=self.request) if form.is_valid(): - # 登录成功,清理缓存并记录日志 delete_sidebar_cache() 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) # 设置会话有效期 + self.request.session.set_expiry(self.login_ttl) return super(LoginView, self).form_valid(form) + # return HttpResponseRedirect('/') else: - # 登录失败,重新显示表单 return self.render_to_response({ 'form': form }) def get_success_url(self): - """ - 获取登录成功后的跳转URL - 验证重定向URL的安全性,防止开放重定向攻击 - """ redirect_to = self.request.POST.get(self.redirect_field_name) - # 验证重定向URL是否安全 if not url_has_allowed_host_and_scheme( url=redirect_to, allowed_hosts=[ self.request.get_host()]): - redirect_to = self.success_url # 不安全则使用默认URL + redirect_to = self.success_url return redirect_to def account_result(request): - """ - 账户操作结果页面视图 - - 显示注册成功或邮箱验证成功的结果信息 - 处理邮箱验证链接的验证逻辑 - """ - type = request.GET.get('type') # 操作类型:register或validation - id = request.GET.get('id') # 用户ID + type = request.GET.get('type') + id = request.GET.get('id') user = get_object_or_404(get_user_model(), id=id) logger.info(type) - - # 如果用户已激活,直接跳转到首页 if user.is_active: return HttpResponseRedirect('/') - - # 处理注册结果或邮箱验证 if type and type in ['register', 'validation']: if type == 'register': - # 注册成功页面 content = ''' 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 ''' title = '注册成功' else: - # 邮箱验证处理 c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = request.GET.get('sign') - # 验证签名防止篡改 if sign != c_sign: return HttpResponseForbidden() - # 激活用户账户 user.is_active = True user.save() content = ''' 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 ''' title = '验证成功' - # 渲染结果页面 return render(request, 'account/result.html', { 'title': title, 'content': content }) else: - # 无效的操作类型,跳转到首页 return HttpResponseRedirect('/') class ForgetPasswordView(FormView): - """ - 忘记密码重置视图 - - 处理用户忘记密码后的密码重置流程 - 验证验证码后更新用户密码 - """ - form_class = ForgetPasswordForm # 忘记密码表单 - template_name = 'account/forget_password.html' # 模板路径 + 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.password = make_password(form.cleaned_data["new_password2"]) blog_user.save() - return HttpResponseRedirect('/login/') # 重定向到登录页面 + return HttpResponseRedirect('/login/') else: - # 表单验证失败,重新显示表单 return self.render_to_response({'form': form}) class ForgetPasswordEmailCode(View): - """ - 忘记密码验证码发送视图 - - 处理忘记密码流程中的验证码发送请求 - """ def post(self, request: HttpRequest): - """ - POST请求处理 - - 验证邮箱地址并发送密码重置验证码 - """ form = ForgetPasswordCodeForm(request.POST) if not form.is_valid(): - return HttpResponse("错误的邮箱") # 邮箱格式错误 - + 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") # 发送成功 \ No newline at end of file + return HttpResponse("ok") -- 2.34.1 From e769baf8caaf200c59294cdb4ae1a05854dceda8 Mon Sep 17 00:00:00 2001 From: hyt <691385292@qq.com> Date: Sun, 9 Nov 2025 21:24:08 +0800 Subject: [PATCH 3/3] =?UTF-8?q?hyt=5Faccounts=20APP=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/accounts/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 152 -> 0 bytes .../__pycache__/admin.cpython-312.pyc | Bin 3173 -> 0 bytes src/accounts/__pycache__/apps.cpython-312.pyc | Bin 401 -> 0 bytes .../__pycache__/forms.cpython-312.pyc | Bin 5825 -> 0 bytes .../__pycache__/models.cpython-312.pyc | Bin 2221 -> 0 bytes src/accounts/__pycache__/urls.cpython-312.pyc | Bin 1293 -> 0 bytes .../user_login_backend.cpython-312.pyc | Bin 1512 -> 0 bytes .../__pycache__/utils.cpython-312.pyc | Bin 2196 -> 0 bytes .../__pycache__/views.cpython-312.pyc | Bin 10721 -> 0 bytes src/accounts/admin.py | 62 +++++-- src/accounts/apps.py | 10 +- src/accounts/forms.py | 82 ++++++++- src/accounts/migrations/0001_initial.py | 104 ----------- ...s_remove_bloguser_created_time_and_more.py | 81 --------- src/accounts/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-312.pyc | Bin 4185 -> 0 bytes ...user_created_time_and_more.cpython-312.pyc | Bin 1901 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 163 -> 0 bytes src/accounts/models.py | 47 ++++- .../__pycache__/__init__.cpython-312.pyc | Bin 165 -> 0 bytes src/accounts/urls.py | 60 ++++--- src/accounts/user_login_backend.py | 54 +++++- src/accounts/utils.py | 66 +++++-- src/accounts/views.py | 170 ++++++++++++++---- 25 files changed, 446 insertions(+), 291 deletions(-) delete mode 100644 src/accounts/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/admin.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/apps.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/forms.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/models.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/urls.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/user_login_backend.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/utils.cpython-312.pyc delete mode 100644 src/accounts/__pycache__/views.cpython-312.pyc delete mode 100644 src/accounts/migrations/0001_initial.py delete mode 100644 src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py delete mode 100644 src/accounts/migrations/__init__.py delete mode 100644 src/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc delete mode 100644 src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-312.pyc delete mode 100644 src/accounts/migrations/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/accounts/templatetags/__pycache__/__init__.cpython-312.pyc diff --git a/src/accounts/__init__.py b/src/accounts/__init__.py index e69de29..4b3a918 100644 --- a/src/accounts/__init__.py +++ b/src/accounts/__init__.py @@ -0,0 +1 @@ +#hyt: \ No newline at end of file diff --git a/src/accounts/__pycache__/__init__.cpython-312.pyc b/src/accounts/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index a8d19b85463826bc89669a9881a3c5696fadb01d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152 zcmX@j%ge<81ceMYGC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!GIz0xDb7zTiSei` zQ3xo?&q_@$i775hj&aFK%uCOA%E?cUNlZ@8FU>0{j){-Y%*!l^kJl@xyv1RYo1ape ZlWJGQ3N(iih>JmtkIamWj77{q76A6kB-Q`` diff --git a/src/accounts/__pycache__/admin.cpython-312.pyc b/src/accounts/__pycache__/admin.cpython-312.pyc deleted file mode 100644 index 2e9919a87f776616434d5459fce62e987154cd3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3173 zcmb7GO>7&-6`t8YF8@qfVJ%9IUADGtubnEbqqt4spC*yN1G;u%}M#0TGq@`B9WJgqpuJr?b2!~S#0NjUygY*FElft znRV5^^nj4&K$>^uzO>LhU=%*Xi3a8Be86fUFzFwKE_>0bd3ER@^uze-AXhU zvdY|46@U@o9_H*yC1}*6aLz{LOe{9Jg4=?%-Jw!j?H)Y5sxd?{8v{~F zP6|9hh#;1?J_d4|v`7{H`&+e#_TBOwn7mE@k6BUzinpZ}W#cW09c$5+bo1M=xua16 zSW?w?uNCtVx=b{u53R)7&%p)&Lz(62B*fp^Vb3ldN~B@g3Ci^#D%b3&a>G!0uHw6P z-E}O-j%+RiRv4a^7hbb{4`hI?D~ttU{+ zkzYX+913aP!wE!6s`ew3XcENAfG9aK`XZT22}F6sQCgCPtBMBJ2H@#_Zxm)=B-Pog zkWyMDsMWkkIGm>i)I%?FA#7^cSacc0!~UtubDEZVPA(X8%LSa zfC9j>67; zb#eW~rN5{f)30}?Uw<$(w>$e00TW3ME z_d^!C+b`rIZNa%M>OuOvPk&7*X~HXG){xjq z(8e3dt9y}U^%8iN}FM=#wu zzNQ;@Pps>PID?_>cN!jwp=tIkl5Yap4NrsxLxTAlqWb`zk0n{1d_;ge&QSIALjojr z5o+#E@V1z2w33QCjv0e1Hrj6cOc za3Qg>vNJ&&D|h*r;>~-Rc{?-vZrekUv4G9vgUp{w^fBm2eJaTs7%+?>K@Sl$xB;fE zfaxK=>205cNIhBSE$vTzccro1kY#|=K-2Z}1jq4waxnH;d diff --git a/src/accounts/__pycache__/forms.cpython-312.pyc b/src/accounts/__pycache__/forms.cpython-312.pyc deleted file mode 100644 index 67a1db99d16ff4d90889243f285fa817fc64fe76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5825 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 P_>eTxSp18?Bgpz6s+JzJ diff --git a/src/accounts/__pycache__/urls.cpython-312.pyc b/src/accounts/__pycache__/urls.cpython-312.pyc deleted file mode 100644 index d616a53a7b0864814d5d5a168ab35ce381e990e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1293 zcmb7C&rcIU6rSymZK0GFS{N{mNFYFswy88B8iEFqcpyqVcxW1$&F<14?sn_URzfwR z{{f!(8%#NPduAu8a$?Tk&Xa-IMWf?A5%wm=OVs8J`QO zF2$#MO5v{P`Cf{g<1SMC`+aq|jV+~L38-RG*dM4vXKZn`AgD##@x|{y)du%Nb$7dh z>x4mP^rAu=|4FCbp(#U`Gz4WBG{&xKD3MD%ml6fuwX3|p-Zj#rF$x;7|J1O@iRO6ukrR=sWN>x*r z$P{R>cR+u_=h!U>!Xb(sqKO|U0^n<4YVPDI9f!&$Qy zo^9czg_BL3vSZVXFvAGZM)al~9dAYFtmvFQKFJQIcx40M;-##pg%>Qm(8PD`*los0 jb{HwnNLx7F!iyGOY~m$5c84+2$CHuJ(E9+H1jD}pWqwRk diff --git a/src/accounts/__pycache__/user_login_backend.cpython-312.pyc b/src/accounts/__pycache__/user_login_backend.cpython-312.pyc deleted file mode 100644 index 216a07ff6d193c4cd1323b2637edc75a62614ea5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmaJ>UuYaf82@H=_x>c8Y-LIAq*gCYDOnOPAShKrYeHIm@f3;=Jr0)5?l!s0-EA_n zXL98VwM0oD)I3z9Z+Q?=BB4Ghlv4U8_>vGI=hA{;)7ZD@DU?3>&EB3I_3X#)@B3!v zn{Q^m?>F;PUtbTPEJ9;$N&@&zG}@wsM)M^!*1-mwl%Py`0p!Lr6}pDSG6gFm>Yh&pMt_%QWM#Q8B%6_L1aKv$S3o*#8*ijL)R zU&5;S{nngg%^M3Q=SwcL{lv8E@T}{dT`q7B3m|i+bkUD8XR+#Vj|npQv46cI^Ghb1 z<-RPI;Sn4Z|7Oigzr$RyVxBWAKV38{v+h*Moz0t;m&j-xG zo+>@6g%V`;R-Z*``1RUooNgn`+RwOVC34l zUkB40BijR`tLF}>5>GX>@$08|wc$N&bVnQA)5dqi_v($42&>Z#RohdK@2JN&PW`No zHIhfydJiQy^5UcW>akt*g*|m_M;-eUNIdn6mOPYDKjzQix}Ur<8HGF1o$FqnD6k`>wW{0oKX4q|KZ2-5Db)@`Q9@49((=r3>@A9593> za0=@apd#p$9T9erSh2Nqnhl`xW9?gi>BQ5D)Aky~Is6R56*#0)JatzadLs8XPHk)B zyV}HdV&c)=|4(v1#<)6s623}Kf^?f8=ro%}uEq>QP~9;69z(oD)snCihOt;ROW{b= zFl^T{3?{b2o)r*00cb#QEGBXmfJk{z(G~cEQYF@m^(d)kN>)-#yj_7$R#$w@4mh2$ z+=|BvGXWc0SM9kjBbkumoCzwlRuemD#cU;ZqP6+cYzPO$%fOQe$T33hLFOL3{8wav Nq&CL>22{bse*js6Y*hdN diff --git a/src/accounts/__pycache__/utils.cpython-312.pyc b/src/accounts/__pycache__/utils.cpython-312.pyc deleted file mode 100644 index 653df06632a0f66127a8694fab9b24bcfc9ba980..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2196 zcmah~Yitx%6uvY2>~{O4yxOXhkP2&R1>+-a2%?}7V_IX8#7&dUuyfl^ncZ3M+y%E; z%oZD3w9qwHSfJry0Fjz5{7{~X|NR3Zk&KdP>}+>`v84R+m*?KuZd+pXCUft3-E+^m z=euWqs;crK7$2Z}{RdnK{l%F1;VBVwIUtT81u24tf@~EvAtDAvfzb}l5pf2coOWui zh&$-!vLE@z!@7b^j}hNFfWtKGU}u*G^T7DI%`hE7v~*zziZFj!ikK8sVbG1x zUI#7jO`<*_DJC6LC5eC%j({`%5xB9&KP#k$3L^*e(?3|*{j;Zr3Kz!m*&!f&5{y?# zpV7f7#+8EXkaCC4TH{9w`=)27FO{{Wm^&np&s_g*=F0WLmzlDbnscb)6!U<)!`99O2BU>YaUu*ig^!RpMB(gDO`q$JT)2{n5~K!mXC zWtQ{;QWlWZjS^zO5w8bxyJqaBbD6A_K9`$3ko#hk7j`K;ct9OeK6^QLY2Rbu__y-E z%OhWamvwGm?!+XxFH9WGXS3GDOZl4<*6|ZIYf!?^>|{VH!zzFCXklb*_SkS~&za%b z^!}ME2dy&~tn?_*4{nTE*Cz7WvxSk7*@^Q+Rt=o{rKtz3Vn zX2UJ-hN1&`8%aHwE~$RGPjrH2arFllV+3?;BvDeJ4BmVUK<9XTEb1CUK#a3@0?lIf z`)QVAC%hcC+jPL=H7j^(Axfd485kaqVg$s6{0(;ge9B(fU|-V{AbkctgJqvWGtP>U zZA05eaeDh?`>#!{x0_n;I-j-EEVn$cEQ7oinEy|6sBum1NMQdN^-kVP$EYSjrTYX9m0x1s+ogY+i)kemo{$9yB29;ItM%q z-cC$!SBy1CS>re&>|S}?Un3r$4K?q^h_1vnyosy>JxkNbz*KYyg76!vx{sc;(3<C^Xxm=FA%>yN-69-*UTo{Ugvd H^T_`J;J{~f diff --git a/src/accounts/__pycache__/views.cpython-312.pyc b/src/accounts/__pycache__/views.cpython-312.pyc deleted file mode 100644 index f9aa82693bb5636a97aee65332d00ea9135a994b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10721 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 diff --git a/src/accounts/admin.py b/src/accounts/admin.py index 32e483c..1258fb8 100644 --- a/src/accounts/admin.py +++ b/src/accounts/admin.py @@ -1,3 +1,5 @@ +# hyt: + from django import forms from django.contrib.auth.admin import UserAdmin from django.contrib.auth.forms import UserChangeForm @@ -9,15 +11,27 @@ from .models import BlogUser class BlogUserCreationForm(forms.ModelForm): + """ + 博客用户创建表单 + + 功能:处理新用户注册时的数据验证和保存 + 扩展了Django标准用户创建流程,添加密码确认和来源记录 + """ password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: model = BlogUser - fields = ('email',) + fields = ('email',) # 创建用户时只需要邮箱字段 def clean_password2(self): - # Check that the two password entries match + """ + 密码确认验证 + + 功能:验证两次输入的密码是否一致 + 返回:验证通过的密码 + 异常:当密码不匹配时抛出ValidationError + """ password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: @@ -25,28 +39,50 @@ class BlogUserCreationForm(forms.ModelForm): return password2 def save(self, commit=True): - # Save the provided password in hashed format + """ + 保存用户信息 + + 功能:处理密码哈希化和用户来源记录 + 参数:commit - 是否立即保存到数据库 + 返回:保存后的用户对象 + """ user = super().save(commit=False) - user.set_password(self.cleaned_data["password1"]) + user.set_password(self.cleaned_data["password1"]) # 密码哈希化 if commit: - user.source = 'adminsite' + user.source = 'adminsite' # 标记用户来源为管理后台 user.save() return user class BlogUserChangeForm(UserChangeForm): + """ + 博客用户信息修改表单 + + 功能:处理用户信息的编辑和更新 + 继承自Django标准UserChangeForm,支持所有字段编辑 + """ + class Meta: model = BlogUser - fields = '__all__' - field_classes = {'username': UsernameField} + fields = '__all__' # 包含所有字段 + field_classes = {'username': UsernameField} # 用户名字段使用特定类型 def __init__(self, *args, **kwargs): + """初始化表单,设置字段属性""" super().__init__(*args, **kwargs) class BlogUserAdmin(UserAdmin): - form = BlogUserChangeForm - add_form = BlogUserCreationForm + """ + 博客用户管理后台配置 + + 功能:自定义Django管理后台的用户管理界面 + 扩展了默认的用户管理功能,优化显示字段和排序 + """ + form = BlogUserChangeForm # 使用自定义修改表单 + add_form = BlogUserCreationForm # 使用自定义创建表单 + + # 列表页显示字段 list_display = ( 'id', 'nickname', @@ -54,6 +90,8 @@ class BlogUserAdmin(UserAdmin): 'email', 'last_login', 'date_joined', - 'source') - list_display_links = ('id', 'username') - ordering = ('-id',) + 'source' + ) + + list_display_links = ('id', 'username') # 可点击链接的字段 + ordering = ('-id',) # 按ID降序排列 diff --git a/src/accounts/apps.py b/src/accounts/apps.py index 9b3fc5a..57a0980 100644 --- a/src/accounts/apps.py +++ b/src/accounts/apps.py @@ -1,5 +1,13 @@ +# hyt: + from django.apps import AppConfig class AccountsConfig(AppConfig): - name = 'accounts' + """ + 账户应用配置类 + + 功能:定义Django账户应用的配置信息 + 继承自AppConfig,用于应用初始化和元数据配置 + """ + name = 'accounts' # 应用名称,对应INSTALLED_APPS中的配置 diff --git a/src/accounts/forms.py b/src/accounts/forms.py index fce4137..5c510d5 100644 --- a/src/accounts/forms.py +++ b/src/accounts/forms.py @@ -1,3 +1,5 @@ +# hyt: + from django import forms from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.forms import AuthenticationForm, UserCreationForm @@ -9,18 +11,37 @@ from .models import BlogUser class LoginForm(AuthenticationForm): + """ + 用户登录表单 + + 功能:处理用户登录认证,自定义表单控件样式 + 继承自Django标准AuthenticationForm,添加Bootstrap样式支持 + """ + def __init__(self, *args, **kwargs): + """初始化表单,设置用户名和密码输入框的样式和占位符""" super(LoginForm, self).__init__(*args, **kwargs) + # 设置用户名输入框样式 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + # 设置密码输入框样式 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) class RegisterForm(UserCreationForm): + """ + 用户注册表单 + + 功能:处理新用户注册,包含用户名、邮箱和密码验证 + 扩展Django标准UserCreationForm,添加邮箱验证和表单样式 + """ + def __init__(self, *args, **kwargs): + """初始化表单,设置所有字段的Bootstrap样式和占位符""" super(RegisterForm, self).__init__(*args, **kwargs) + # 设置各字段的表单控件样式 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -31,17 +52,33 @@ class RegisterForm(UserCreationForm): attrs={'placeholder': "repeat password", "class": "form-control"}) def clean_email(self): + """ + 邮箱唯一性验证 + + 功能:验证邮箱是否已被注册 + 返回:验证通过的邮箱 + 异常:邮箱已存在时抛出ValidationError + """ email = self.cleaned_data['email'] if get_user_model().objects.filter(email=email).exists(): raise ValidationError(_("email already exists")) return email class Meta: - model = get_user_model() - fields = ("username", "email") + """表单元数据配置""" + model = get_user_model() # 使用当前激活的用户模型 + fields = ("username", "email") # 表单包含的字段 class ForgetPasswordForm(forms.Form): + """ + 忘记密码重置表单 + + 功能:处理密码重置流程,包含邮箱验证、验证码校验和新密码设置 + 用于用户通过邮箱和验证码找回密码的场景 + """ + + # 新密码字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -52,6 +89,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 确认密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -62,6 +100,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 邮箱字段 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -72,6 +111,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 验证码字段 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -83,24 +123,43 @@ class ForgetPasswordForm(forms.Form): ) def clean_new_password2(self): + """ + 新密码确认验证 + + 功能:验证两次输入的新密码是否一致并符合密码策略 + 返回:验证通过的密码 + 异常:密码不匹配或不符合策略时抛出ValidationError + """ password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) - password_validation.validate_password(password2) + password_validation.validate_password(password2) # Django密码策略验证 return password2 def clean_email(self): + """ + 邮箱存在性验证 + + 功能:验证邮箱是否在系统中注册 + 返回:验证通过的邮箱 + 异常:邮箱未注册时抛出ValidationError + """ user_email = self.cleaned_data.get("email") - if not BlogUser.objects.filter( - email=user_email - ).exists(): - # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + if not BlogUser.objects.filter(email=user_email).exists(): + # 安全提示:这里的报错会暴露邮箱是否注册,可根据安全需求调整 raise ValidationError(_("email does not exist")) return user_email def clean_code(self): + """ + 验证码校验 + + 功能:验证邮箱验证码的有效性 + 返回:验证通过的验证码 + 异常:验证码无效时抛出ValidationError + """ code = self.cleaned_data.get("code") error = utils.verify( email=self.cleaned_data.get("email"), @@ -112,6 +171,13 @@ class ForgetPasswordForm(forms.Form): class ForgetPasswordCodeForm(forms.Form): + """ + 忘记密码验证码请求表单 + + 功能:用于请求发送密码重置验证码,仅包含邮箱字段 + 简化表单,专门用于验证码发送流程 + """ + email = forms.EmailField( - label=_('Email'), + label=_('Email'), # 邮箱标签 ) diff --git a/src/accounts/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py deleted file mode 100644 index e13c533..0000000 --- a/src/accounts/migrations/0001_initial.py +++ /dev/null @@ -1,104 +0,0 @@ -#hyt: -# Generated by Django 4.1.7 on 2023-03-02 07:14 - -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - """ - BlogUser 模型的初始迁移文件 - - 创建自定义用户模型 BlogUser,扩展 Django 内置 User 模型 - 添加了昵称、时间戳和来源字段,支持中文显示和自定义排序 - """ - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='BlogUser', - fields=[ - # 主键字段 - 使用 BigAutoField 作为自增主键 - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - - # 认证相关字段 - Django 内置用户认证系统必需字段 - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - - # 权限相关字段 - 用户权限和状态管理 - ('is_superuser', models.BooleanField( - default=False, - help_text='Designates that this user has all permissions without explicitly assigning them.', - verbose_name='superuser status' - )), - - # 用户基本信息字段 - 用户名、姓名、邮箱等 - ('username', models.CharField( - error_messages={'unique': 'A user with that username already exists.'}, - help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', - max_length=150, - unique=True, - validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], - verbose_name='username' - )), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - - # 状态字段 - 用户状态标识 - ('is_staff', models.BooleanField( - default=False, - help_text='Designates whether the user can log into this admin site.', - verbose_name='staff status' - )), - ('is_active', models.BooleanField( - default=True, - help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', - verbose_name='active' - )), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - - # 自定义扩展字段 - 博客用户特有字段 - ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), - - # 权限关联字段 - 用户组和权限的多对多关系 - ('groups', models.ManyToManyField( - blank=True, - help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', - related_name='user_set', - related_query_name='user', - to='auth.group', - verbose_name='groups' - )), - ('user_permissions', models.ManyToManyField( - blank=True, - help_text='Specific permissions for this user.', - related_name='user_set', - related_query_name='user', - to='auth.permission', - verbose_name='user permissions' - )), - ], - options={ - # 模型元选项 - 定义模型在 admin 中的显示和排序 - 'verbose_name': '用户', # 单数显示名称 - 'verbose_name_plural': '用户', # 复数显示名称 - 'ordering': ['-id'], # 按 ID 降序排列 - 'get_latest_by': 'id', # 获取最新记录的依据字段 - }, - managers=[ - # 模型管理器 - 使用 Django 默认的用户管理器 - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] \ No newline at end of file diff --git a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py deleted file mode 100644 index 22eb985..0000000 --- a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py +++ /dev/null @@ -1,81 +0,0 @@ -#hyt: -# Generated by Django 4.2.5 on 2023-09-06 13:13 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - """ - BlogUser 模型结构调整迁移文件 - - 对 accounts 应用的 BlogUser 模型进行字段优化和国际化改进: - 1. 调整时间字段命名,统一使用英文命名规范 - 2. 更新模型选项,改进 Admin 后台显示 - 3. 字段标签国际化,为多语言支持做准备 - """ - - dependencies = [ - ('accounts', '0001_initial'), # 依赖于初始迁移文件 - ] - - operations = [ - # 模型选项调整 - 更新 Admin 后台显示配置 - migrations.AlterModelOptions( - name='bloguser', - options={ - 'get_latest_by': 'id', # 按 ID 获取最新记录 - 'ordering': ['-id'], # 按 ID 降序排列 - 'verbose_name': 'user', # 单数显示名称(英文) - 'verbose_name_plural': 'user', # 复数显示名称(英文) - }, - ), - - # 字段清理 - 移除旧的时间字段 - migrations.RemoveField( - model_name='bloguser', - name='created_time', # 移除旧的创建时间字段 - ), - migrations.RemoveField( - model_name='bloguser', - name='last_mod_time', # 移除旧的修改时间字段 - ), - - # 字段添加 - 新增标准化时间字段 - migrations.AddField( - model_name='bloguser', - name='creation_time', - field=models.DateTimeField( - default=django.utils.timezone.now, # 默认值为当前时间 - verbose_name='creation time' # 字段显示名称(英文) - ), - ), - migrations.AddField( - model_name='bloguser', - name='last_modify_time', - field=models.DateTimeField( - default=django.utils.timezone.now, # 默认值为当前时间 - verbose_name='last modify time' # 字段显示名称(英文) - ), - ), - - # 字段调整 - 更新字段标签为英文 - migrations.AlterField( - model_name='bloguser', - name='nickname', - field=models.CharField( - blank=True, # 允许为空 - max_length=100, # 最大长度100字符 - verbose_name='nick name' # 字段显示名称(英文) - ), - ), - migrations.AlterField( - model_name='bloguser', - name='source', - field=models.CharField( - blank=True, # 允许为空 - max_length=100, # 最大长度100字符 - verbose_name='create source' # 字段显示名称(英文) - ), - ), - ] \ No newline at end of file diff --git a/src/accounts/migrations/__init__.py b/src/accounts/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc b/src/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 95a7532cf8a7c71c7336beca79b4a8f47a6becac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4185 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 diff --git a/src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-312.pyc b/src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-312.pyc deleted file mode 100644 index 82e80cab8388b555deb6a21f3461e464f5982b7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1901 zcmcIkOK;mo5MDk+QH+#Gw4F!o*oxz-V1QUoPHj;XO)I2E+ayg8AcY`+ptviV3PmbQ z%BkEOS|EoWbL*u)AVCE5>Yq@c7ZWfbVSDT;Hvx7LAgAswMaQ+}wwIE?a%Ohsn+Lz$ z-wFi{(B}~tZR8aI{$NV_WG2GiO)C5ZAb^4gIznCOh;>n5y68!DSp=U0C_M#GMnapT zr2xtIrIdW@nZ9UbUNOZGiPmu)Y(;M1heYWF5P9KkJ8?|bGZSU+6Om;{pe{gA7op$? zbx8zBo|X%WP=c}}zzltrpXEXJC5>hekH{zT;gv?HgWP}c(vEnYK8%vc6zdetqqg2#vJr|ET^`Jd`2o><4il&VEo;&m)e+0)?+oE^-i zy&UOkI@;NPL_0SP&B@k_{qx2JlDm}>j0;d-u zOhgxMNmpG+WTqN2DL5!HJu5oaA~T0Iv+MP+<Qxgz^eq1& zJ+{`jriXkd+Tgi}5(YhNqe)4qoT#^r5>ZXwk7*LkWS#UpMCH6`KI&N>LuDF7UF1XL z+b#+*yC_KMh8H1xi_PQCWLl^0p-!-gZt_ml8xV2{%gnt{qjS7ZrOA)^kMzp3%22Pwdgb}%FHc@P8CGj?wKlA- z$JO%~~T$g|Fm^ktTHIo6k-&u(A-^~+uT zn-hF59QD1kt2Ywg4=(A-4`1Nbbd-idatSw9VUsg0;m_Q}$oaCG(gj!2IDC~!9HvSo vqJET6`D1*Ys`S@@aEUHsNf3nB3J{Fn!NR`so}fI_hQ*b*xbi2U3J3TLEs@s% diff --git a/src/accounts/migrations/__pycache__/__init__.cpython-312.pyc b/src/accounts/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 7090758445eb4b6b54ee58e6698b8fd3b5b3705f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmX@j%ge<81ceMYGC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&obXDb7zTiSei` zQ3xo?&q_@$i775hj&aFK%uCOA%E?cUNlZ@8FU>0{j>*kTFG?)Q%+D*1iI30B%PfhH k*DI*J#bJ}1pHiBWYFESxG?EdBi$RQ!%#4hTMa)1J00&zsdH?_b diff --git a/src/accounts/models.py b/src/accounts/models.py index 3baddbb..e8fe7cd 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -1,3 +1,5 @@ +# hyt: + from django.contrib.auth.models import AbstractUser from django.db import models from django.urls import reverse @@ -9,27 +11,64 @@ from djangoblog.utils import get_current_site # Create your models here. class BlogUser(AbstractUser): + """ + 博客用户模型 + + 功能:扩展Django标准用户模型,添加博客系统特有字段 + 继承自AbstractUser,包含认证系统基础字段和自定义业务字段 + """ + + # 用户昵称 - 可选的显示名称 nickname = models.CharField(_('nick name'), max_length=100, blank=True) + + # 创建时间 - 记录用户注册时间 creation_time = models.DateTimeField(_('creation time'), default=now) + + # 最后修改时间 - 记录用户信息最后更新时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + # 创建来源 - 记录用户注册渠道(如网站、移动端等) source = models.CharField(_('create source'), max_length=100, blank=True) def get_absolute_url(self): + """ + 获取用户详情页的绝对URL + + 返回:用户作者详情页的URL路径 + 用于Django admin和模板中的链接生成 + """ return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) def __str__(self): + """ + 对象字符串表示 + + 返回:用户的邮箱地址 + 用于Django admin和其他显示场景 + """ return self.email def get_full_url(self): + """ + 获取用户的完整URL(包含域名) + + 返回:包含协议和域名的完整用户URL + 用于生成外部可访问的用户主页链接 + """ site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - ordering = ['-id'] - verbose_name = _('user') - verbose_name_plural = verbose_name - get_latest_by = 'id' + """ + 模型元数据配置 + + 定义模型在数据库和Django admin中的行为 + """ + ordering = ['-id'] # 默认按ID降序排列 + verbose_name = _('user') # 单数显示名称 + verbose_name_plural = verbose_name # 复数显示名称(与单数相同) + get_latest_by = 'id' # 获取最新记录的依据字段 \ No newline at end of file diff --git a/src/accounts/templatetags/__pycache__/__init__.cpython-312.pyc b/src/accounts/templatetags/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 3061f4f66f141d3535d5f7a22c5eaec4ba4edca4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmX@j%ge<81ceMYGC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&fVWDb7zTiSei` zQ3xo?&q_@$i775hj&aFK%uCOA%E?cUNlZ@8FU>0{jwwmaEyzhMNi9iCFOG?i&& typing.Optional[str]: - """验证code是否有效 - Args: - email: 请求邮箱 - code: 验证码 - Return: - 如果有错误就返回错误str - Node: - 这里的错误处理不太合理,应该采用raise抛出 - 否测调用方也需要对error进行处理 + """ + 验证验证码有效性 + + 功能:验证用户输入的验证码与缓存中的验证码是否匹配 + 参数: + email: 用户邮箱地址,作为缓存键 + code: 用户输入的验证码 + + 返回: + str: 验证失败时返回错误信息 + None: 验证成功时返回None + + 注意:当前错误处理方式不够合理,建议改为异常抛出机制 + 这样调用方可以通过try-except处理错误,避免条件判断 """ cache_code = get_code(email) if cache_code != code: @@ -40,10 +53,29 @@ def verify(email: str, code: str) -> typing.Optional[str]: def set_code(email: str, code: str): - """设置code""" + """ + 设置验证码到缓存 + + 功能:将验证码存储到缓存系统,使用邮箱作为键 + 参数: + email: 邮箱地址,作为缓存键 + code: 验证码内容,作为缓存值 + + 缓存配置:验证码有效期为5分钟(_code_ttl) + """ cache.set(email, code, _code_ttl.seconds) def get_code(email: str) -> typing.Optional[str]: - """获取code""" - return cache.get(email) + """ + 从缓存获取验证码 + + 功能:根据邮箱地址从缓存中获取对应的验证码 + 参数: + email: 邮箱地址,作为缓存键 + + 返回: + str: 存在验证码时返回验证码内容 + None: 验证码不存在或已过期时返回None + """ + return cache.get(email) \ No newline at end of file diff --git a/src/accounts/views.py b/src/accounts/views.py index ae67aec..e70429e 100644 --- a/src/accounts/views.py +++ b/src/accounts/views.py @@ -1,3 +1,5 @@ +# hyt: + import logging from django.utils.translation import gettext_lazy as _ from django.conf import settings @@ -32,28 +34,46 @@ logger = logging.getLogger(__name__) # Create your views here. class RegisterView(FormView): - form_class = RegisterForm - template_name = 'account/registration_form.html' + """ + 用户注册视图 + + 功能:处理新用户注册流程,包括表单验证、用户创建和邮箱验证 + 继承自FormView,使用自定义注册表单 + """ + form_class = RegisterForm # 使用自定义注册表单 + template_name = 'account/registration_form.html' # 注册页面模板 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): + """CSRF保护装饰器,防止跨站请求伪造""" return super(RegisterView, self).dispatch(*args, **kwargs) def form_valid(self, form): + """ + 表单验证通过处理 + + 功能:处理注册表单验证通过后的业务流程 + 包括:用户创建、邮箱验证链接生成和验证邮件发送 + """ if form.is_valid(): + # 创建用户但不立即保存到数据库 user = form.save(False) - user.is_active = False - user.source = 'Register' - user.save(True) + user.is_active = False # 用户未激活,需要邮箱验证 + user.source = 'Register' # 标记用户来源为注册 + user.save(True) # 保存用户到数据库 + + # 生成邮箱验证链接 site = get_current_site().domain sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) if settings.DEBUG: - site = '127.0.0.1:8000' + site = '127.0.0.1:8000' # 调试模式下使用本地地址 + path = reverse('account:result') url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) + # 构建验证邮件内容 content = """

请点击下面链接验证您的邮箱

@@ -64,6 +84,7 @@ class RegisterView(FormView): 如果上面链接无法打开,请将此链接复制至浏览器。 {url} """.format(url=url) + # 发送验证邮件 send_email( emailto=[ user.email, @@ -71,102 +92,152 @@ class RegisterView(FormView): title='验证您的电子邮箱', content=content) + # 跳转到注册结果页面 url = reverse('accounts:result') + \ '?type=register&id=' + str(user.id) return HttpResponseRedirect(url) else: + # 表单验证失败,重新渲染表单页面 return self.render_to_response({ 'form': form }) class LogoutView(RedirectView): - url = '/login/' + """ + 用户注销视图 + + 功能:处理用户注销流程,清理会话和缓存 + 继承自RedirectView,注销后重定向到登录页面 + """ + url = '/login/' # 注销后重定向地址 @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): + """禁用缓存,确保注销操作实时生效""" return super(LogoutView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): - logout(request) - delete_sidebar_cache() + """ + 处理GET请求注销 + + 功能:执行用户注销操作,清理侧边栏缓存 + """ + logout(request) # Django内置注销函数 + delete_sidebar_cache() # 清理侧边栏缓存 return super(LogoutView, self).get(request, *args, **kwargs) class LoginView(FormView): - form_class = LoginForm - 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) - @method_decorator(never_cache) + """ + 用户登录视图 + + 功能:处理用户登录认证,支持记住登录状态 + 继承自FormView,使用自定义登录表单 + """ + form_class = LoginForm # 使用自定义登录表单 + 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) # CSRF保护 + @method_decorator(never_cache) # 禁用缓存 def dispatch(self, request, *args, **kwargs): - + """多重装饰器保护登录流程安全""" return super(LoginView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): + """ + 获取模板上下文数据 + + 功能:添加重定向地址到上下文,用于登录后跳转 + """ redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: - redirect_to = '/' + redirect_to = '/' # 默认跳转到首页 kwargs['redirect_to'] = redirect_to return super(LoginView, self).get_context_data(**kwargs) def form_valid(self, form): + """ + 表单验证通过处理 + + 功能:处理登录认证,设置会话过期时间 + """ form = AuthenticationForm(data=self.request.POST, request=self.request) if form.is_valid(): - delete_sidebar_cache() + delete_sidebar_cache() # 清理侧边栏缓存 logger.info(self.redirect_field_name) - auth.login(self.request, form.get_user()) + auth.login(self.request, form.get_user()) # Django内置登录函数 + # 处理"记住我"选项 if self.request.POST.get("remember"): - self.request.session.set_expiry(self.login_ttl) + self.request.session.set_expiry(self.login_ttl) # 设置会话有效期 return super(LoginView, self).form_valid(form) - # return HttpResponseRedirect('/') else: + # 登录失败,重新渲染登录页面 return self.render_to_response({ 'form': form }) def get_success_url(self): + """ + 获取登录成功后的跳转地址 + 功能:验证重定向地址的安全性,防止开放重定向攻击 + """ redirect_to = self.request.POST.get(self.redirect_field_name) + # 验证重定向地址是否安全 if not url_has_allowed_host_and_scheme( url=redirect_to, allowed_hosts=[ self.request.get_host()]): - redirect_to = self.success_url + redirect_to = self.success_url # 不安全则使用默认地址 return redirect_to def account_result(request): - type = request.GET.get('type') - id = request.GET.get('id') + """ + 账户操作结果页面视图 + + 功能:显示注册或邮箱验证的结果信息 + 处理注册成功提示和邮箱验证激活 + """ + type = request.GET.get('type') # 操作类型:register或validation + id = request.GET.get('id') # 用户ID - user = get_object_or_404(get_user_model(), id=id) + user = get_object_or_404(get_user_model(), id=id) # 获取用户对象 logger.info(type) + + # 如果用户已激活,直接跳转到首页 if user.is_active: return HttpResponseRedirect('/') + + # 处理注册和验证两种场景 if type and type in ['register', 'validation']: if type == 'register': + # 注册成功场景 content = ''' 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 ''' title = '注册成功' else: + # 邮箱验证场景 c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = request.GET.get('sign') + # 验证签名防止篡改 if sign != c_sign: return HttpResponseForbidden() - user.is_active = True + user.is_active = True # 激活用户 user.save() content = ''' 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 ''' title = '验证成功' + # 渲染结果页面 return render(request, 'account/result.html', { 'title': title, 'content': content @@ -176,29 +247,56 @@ def account_result(request): class ForgetPasswordView(FormView): - form_class = ForgetPasswordForm - template_name = 'account/forget_password.html' + """ + 忘记密码重置视图 + + 功能:处理用户密码重置流程 + 继承自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() + # 使用Django密码哈希函数设置新密码 blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.save() - return HttpResponseRedirect('/login/') + return HttpResponseRedirect('/login/') # 重定向到登录页面 else: + # 表单验证失败,重新渲染表单 return self.render_to_response({'form': form}) class ForgetPasswordEmailCode(View): + """ + 忘记密码验证码发送视图 + + 功能:处理密码重置验证码的发送请求 + 继承自View,处理POST请求发送验证码邮件 + """ def post(self, request: HttpRequest): + """ + 处理POST请求发送验证码 + + 功能:验证邮箱格式,生成并发送验证码 + """ form = ForgetPasswordCodeForm(request.POST) if not form.is_valid(): - return HttpResponse("错误的邮箱") + return HttpResponse("错误的邮箱") # 邮箱格式错误 + to_email = form.cleaned_data["email"] + # 生成并发送验证码 code = generate_code() - utils.send_verify_email(to_email, code) - utils.set_code(to_email, code) + utils.send_verify_email(to_email, code) # 发送验证邮件 + utils.set_code(to_email, code) # 存储验证码到缓存 - return HttpResponse("ok") + return HttpResponse("ok") # 返回成功响应 -- 2.34.1