refactor: 移除邮件功能

发信能力整体下线,唯一的调用方(忘记密码)前端也从来没接过。

- utils/shortcuts.py 删 send_email,连带四个 django.core.mail 相关 import
- account/tasks.py 整个删除,里面只有 send_email_async 一个 actor
- 删 ApplyResetPasswordAPI / ResetPasswordAPI 及其路由、serializer,
  User.reset_password_token 和 reset_password_token_expire_time 两个
  字段(account/0010)
- 删 SMTPAPI / SMTPTestAPI 及其路由、三个 SMTP serializer、
  SysOptions.smtp_config
- account/templates/reset_password_email.html

options/0003 数据迁移删掉库里的 smtp_config 行:_init_option 会为每个
OptionKey 建行,key 从代码里去掉后它会变成孤儿,而且配过 SMTP 的话
value 里存着明文密码。

保留 ResetUserPasswordAPI(管理端直接重置密码,不发信,前端在用)和
User.email(注册要填)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 23:17:48 -06:00
parent 8d35d5c2de
commit 927f235537
13 changed files with 46 additions and 245 deletions

View File

@@ -0,0 +1,21 @@
# Generated by Django 6.0.4 on 2026-08-06 05:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_remove_user_tfa_token_remove_user_two_factor_auth'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='reset_password_token',
),
migrations.RemoveField(
model_name='user',
name='reset_password_token_expire_time',
),
]

View File

@@ -36,8 +36,6 @@ class User(AbstractBaseUser):
# One of UserType
admin_type = models.TextField(default=AdminType.REGULAR_USER, choices=AdminType.choices)
problem_permission = models.TextField(default=ProblemPermission.NONE, choices=ProblemPermission.choices)
reset_password_token = models.TextField(null=True)
reset_password_token_expire_time = models.DateTimeField(null=True)
# SSO auth token
auth_token = models.TextField(null=True)
session_keys = JSONField(default=list, db_default=models.Value([], output_field=models.JSONField()))

View File

@@ -131,17 +131,6 @@ class EditUserProfileSerializer(serializers.Serializer):
language = serializers.CharField(max_length=32, allow_blank=True, required=False)
class ApplyResetPasswordSerializer(serializers.Serializer):
email = serializers.EmailField()
captcha = serializers.CharField()
class ResetPasswordSerializer(serializers.Serializer):
token = serializers.CharField()
password = serializers.CharField(min_length=6)
captcha = serializers.CharField()
class SSOSerializer(serializers.Serializer):
token = serializers.CharField()

View File

@@ -1,18 +0,0 @@
import logging
import dramatiq
from options.options import SysOptions
from utils.shortcuts import DRAMATIQ_WORKER_ARGS, send_email
logger = logging.getLogger(__name__)
@dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3))
def send_email_async(from_name, to_email, to_name, subject, content):
if not SysOptions.smtp_config:
return
try:
send_email(smtp_config=SysOptions.smtp_config, from_name=from_name, to_email=to_email, to_name=to_name, subject=subject, content=content)
except Exception as e:
logger.exception(e)

View File

@@ -1,31 +0,0 @@
<div>
<table cellpadding="0" align="center"
style="overflow:hidden;background:#fff;margin:0 auto;text-align:left;position:relative;font-size:14px; font-family:'lucida Grande',Verdana;line-height:1.5;box-shadow:0 0 3px #ccc;border:1px solid #ccc;border-radius:5px;border-collapse:collapse;">
<tbody>
<tr>
<th valign="middle"
style="height:38px;color:#fff; font-size:14px;line-height:38px; font-weight:bold;text-align:left;padding:10px 24px 6px; border-bottom:1px solid #467ec3;background:#518bcb;border-radius:5px 5px 0 0;">
{{ website_name }}</th>
</tr>
<tr>
<td>
<div style="padding:20px 35px 40px;">
<h2 style="font-weight:bold;margin-bottom:5px;font-size:14px;">Hello, {{ username }}:</h2>
<p style="margin-top:20px">
Please click <a href="{{ link }}">{{ link }}</a> to reset your password in 20 minutes.
</p>
<p style="margin-top:20px">
To protect your account, please do not use simple passwords.
</p>
<p style="margin-top:20px">
If you still have any questions, please contract system administrator.
</p>
<p style="margin-left:2em;"></p>
<p style="text-indent:0;text-align:right;">{{ website_name }}</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -4,12 +4,10 @@ from utils.captcha.views import CaptchaAPIView
from ..views.oj import (
SSOAPI,
ApplyResetPasswordAPI,
AvatarUploadAPI,
Metrics,
OpenAPIAppkeyAPI,
ProfileProblemDisplayIDRefreshAPI,
ResetPasswordAPI,
SessionManagementAPI,
UserActivityRankAPI,
UserChangeEmailAPI,
@@ -29,8 +27,6 @@ urlpatterns = [
path("register", UserRegisterAPI.as_view()),
path("change_password", UserChangePasswordAPI.as_view()), # DEPRECATED: 前端未调用
path("change_email", UserChangeEmailAPI.as_view()), # DEPRECATED: 前端未调用
path("apply_reset_password", ApplyResetPasswordAPI.as_view()), # DEPRECATED: 前端未调用
path("reset_password", ResetPasswordAPI.as_view()), # DEPRECATED: 前端未调用
path("captcha", CaptchaAPIView.as_view()),
path("check_username_or_email", UsernameOrEmailCheck.as_view()), # DEPRECATED: 前端未调用
path("profile", UserProfileAPI.as_view(), name="user_profile_api"),

View File

@@ -1,15 +1,12 @@
import asyncio
import os
from datetime import timedelta
from importlib import import_module
from django.conf import settings
from django.contrib import auth
from django.db.models import Count, Q
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from options.options import SysOptions
@@ -24,11 +21,9 @@ from utils.shortcuts import datetime2str, rand_str
from ..decorators import login_required
from ..models import AdminType, User, UserProfile
from ..serializers import (
ApplyResetPasswordSerializer,
EditUserProfileSerializer,
ImageUploadForm,
RankInfoSerializer,
ResetPasswordSerializer,
SSOSerializer,
UserChangeEmailSerializer,
UserChangePasswordSerializer,
@@ -37,7 +32,6 @@ from ..serializers import (
UserProfileSerializer,
UserRegisterSerializer,
)
from ..tasks import send_email_async
class UserProfileAPI(AsyncAPIView):
@@ -216,61 +210,6 @@ class UserChangePasswordAPI(APIView):
return self.error("Invalid old password")
# DEPRECATED: 前端未调用 (2026-05-26)
class ApplyResetPasswordAPI(APIView):
@validate_serializer(ApplyResetPasswordSerializer)
def post(self, request):
if request.user.is_authenticated:
return self.error("You have already logged in, are you kidding me? ")
data = request.data
captcha = Captcha(request)
if not captcha.check(data["captcha"]):
return self.error("Invalid captcha")
try:
user = User.objects.get(email__iexact=data["email"])
except User.DoesNotExist:
return self.error("User does not exist")
if user.reset_password_token_expire_time and 0 < int((user.reset_password_token_expire_time - now()).total_seconds()) < 20 * 60:
return self.error("You can only reset password once per 20 minutes")
user.reset_password_token = rand_str()
user.reset_password_token_expire_time = now() + timedelta(minutes=20)
user.save()
render_data = {
"username": user.username,
"website_name": SysOptions.website_name,
"link": f"{SysOptions.website_base_url}/reset-password/{user.reset_password_token}",
}
email_html = render_to_string("reset_password_email.html", render_data)
send_email_async.send(
from_name=SysOptions.website_name_shortcut,
to_email=user.email,
to_name=user.username,
subject="Reset your password",
content=email_html,
)
return self.success("Succeeded")
# DEPRECATED: 前端未调用 (2026-05-26)
class ResetPasswordAPI(APIView):
@validate_serializer(ResetPasswordSerializer)
def post(self, request):
data = request.data
captcha = Captcha(request)
if not captcha.check(data["captcha"]):
return self.error("Invalid captcha")
try:
user = User.objects.get(reset_password_token=data["token"])
except User.DoesNotExist:
return self.error("Token does not exist")
if user.reset_password_token_expire_time < now():
return self.error("Token has expired")
user.reset_password_token = None
user.set_password(data["password"])
user.save()
return self.success("Succeeded")
# DEPRECATED: 前端未调用 (2026-05-26)
class SessionManagementAPI(APIView):
@login_required