diff --git a/account/migrations/0010_remove_user_reset_password_token_and_more.py b/account/migrations/0010_remove_user_reset_password_token_and_more.py
new file mode 100644
index 0000000..8a690d7
--- /dev/null
+++ b/account/migrations/0010_remove_user_reset_password_token_and_more.py
@@ -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',
+ ),
+ ]
diff --git a/account/models.py b/account/models.py
index ef7d3a9..4533f2c 100644
--- a/account/models.py
+++ b/account/models.py
@@ -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()))
diff --git a/account/serializers.py b/account/serializers.py
index 7e3a221..935ba1f 100644
--- a/account/serializers.py
+++ b/account/serializers.py
@@ -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()
diff --git a/account/tasks.py b/account/tasks.py
deleted file mode 100644
index 9e2042b..0000000
--- a/account/tasks.py
+++ /dev/null
@@ -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)
diff --git a/account/templates/reset_password_email.html b/account/templates/reset_password_email.html
deleted file mode 100644
index 54dcc55..0000000
--- a/account/templates/reset_password_email.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
- |
- {{ website_name }} |
-
-
-
-
- Hello, {{ username }}:
-
- Please click {{ link }} to reset your password in 20 minutes.
-
-
- To protect your account, please do not use simple passwords.
-
-
- If you still have any questions, please contract system administrator.
-
-
- {{ website_name }}
-
- |
-
-
-
-
-
\ No newline at end of file
diff --git a/account/urls/oj.py b/account/urls/oj.py
index 5b77b95..05add72 100644
--- a/account/urls/oj.py
+++ b/account/urls/oj.py
@@ -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"),
diff --git a/account/views/oj.py b/account/views/oj.py
index 3848a98..da3fb0c 100644
--- a/account/views/oj.py
+++ b/account/views/oj.py
@@ -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
diff --git a/conf/serializers.py b/conf/serializers.py
index c63149a..22fbe0d 100644
--- a/conf/serializers.py
+++ b/conf/serializers.py
@@ -3,22 +3,6 @@ from utils.api import serializers
from .models import JudgeServer
-class EditSMTPConfigSerializer(serializers.Serializer):
- server = serializers.CharField(max_length=128)
- port = serializers.IntegerField(default=25)
- email = serializers.CharField(max_length=256)
- password = serializers.CharField(max_length=128, required=False, allow_null=True, allow_blank=True)
- tls = serializers.BooleanField()
-
-
-class CreateSMTPConfigSerializer(EditSMTPConfigSerializer):
- password = serializers.CharField(max_length=128)
-
-
-class TestSMTPConfigSerializer(serializers.Serializer):
- email = serializers.EmailField()
-
-
class CreateEditWebsiteConfigSerializer(serializers.Serializer):
website_base_url = serializers.CharField(max_length=128)
website_name = serializers.CharField(max_length=64)
diff --git a/conf/urls/admin.py b/conf/urls/admin.py
index d0985ab..29202dc 100644
--- a/conf/urls/admin.py
+++ b/conf/urls/admin.py
@@ -1,18 +1,14 @@
from django.urls import path
from ..views import (
- SMTPAPI,
DashboardInfoAPI,
JudgeServerAPI,
RandomUsernameAPI,
- SMTPTestAPI,
TestCasePruneAPI,
WebsiteConfigAPI,
)
urlpatterns = [
- path("smtp", SMTPAPI.as_view()), # DEPRECATED: 前端未调用
- path("smtp_test", SMTPTestAPI.as_view()), # DEPRECATED: 前端未调用
path("website", WebsiteConfigAPI.as_view()),
path("random_user", RandomUsernameAPI.as_view()),
path("judge_server", JudgeServerAPI.as_view()),
diff --git a/conf/views.py b/conf/views.py
index 5732e99..dddce53 100644
--- a/conf/views.py
+++ b/conf/views.py
@@ -4,7 +4,6 @@ import os
import random
import re
import shutil
-import smtplib
from datetime import timedelta
from asgiref.sync import sync_to_async
@@ -20,83 +19,19 @@ from problem.models import Problem
from submission.models import Submission
from utils.api import APIView, AsyncAPIView, CSRFExemptAPIView, validate_serializer
from utils.cache import JsonDataLoader
-from utils.shortcuts import get_env, send_email
+from utils.shortcuts import get_env
from utils.websocket import push_config_update
from utils.xss_filter import XSSHtml
from .models import JudgeServer
from .serializers import (
CreateEditWebsiteConfigSerializer,
- CreateSMTPConfigSerializer,
EditJudgeServerSerializer,
- EditSMTPConfigSerializer,
JudgeServerHeartbeatSerializer,
JudgeServerSerializer,
- TestSMTPConfigSerializer,
)
-# DEPRECATED: 前端未调用 (2026-05-26)
-class SMTPAPI(APIView):
- @super_admin_required
- def get(self, request):
- smtp = SysOptions.smtp_config
- if not smtp:
- return self.success(None)
- smtp.pop("password")
- return self.success(smtp)
-
- @super_admin_required
- @validate_serializer(CreateSMTPConfigSerializer)
- def post(self, request):
- SysOptions.smtp_config = request.data
- return self.success()
-
- @super_admin_required
- @validate_serializer(EditSMTPConfigSerializer)
- def put(self, request):
- smtp = SysOptions.smtp_config
- data = request.data
- for item in ["server", "port", "email", "tls"]:
- smtp[item] = data[item]
- if "password" in data:
- smtp["password"] = data["password"]
- SysOptions.smtp_config = smtp
- return self.success()
-
-
-# DEPRECATED: 前端未调用 (2026-05-26)
-class SMTPTestAPI(APIView):
- @super_admin_required
- @validate_serializer(TestSMTPConfigSerializer)
- def post(self, request):
- if not SysOptions.smtp_config:
- return self.error("Please setup SMTP config at first")
- try:
- send_email(
- smtp_config=SysOptions.smtp_config,
- from_name=SysOptions.website_name_shortcut,
- to_name=request.user.username,
- to_email=request.data["email"],
- subject="You have successfully configured SMTP",
- content="You have successfully configured SMTP",
- )
- except smtplib.SMTPResponseException as e:
- # guess error message encoding
- msg = b"Failed to send email"
- try:
- msg = e.smtp_error
- # qq mail
- msg = msg.decode("gbk")
- except Exception:
- msg = msg.decode("utf-8", "ignore")
- return self.error(msg)
- except Exception as e:
- msg = str(e)
- return self.error(msg)
- return self.success()
-
-
class WebsiteConfigAPI(AsyncAPIView):
async def get(self, request):
ret = await SysOptions.aget_many(
diff --git a/options/migrations/0003_remove_smtp_config.py b/options/migrations/0003_remove_smtp_config.py
new file mode 100644
index 0000000..ec86ee7
--- /dev/null
+++ b/options/migrations/0003_remove_smtp_config.py
@@ -0,0 +1,24 @@
+# 邮件功能已整体移除,smtp_config 不再有读写方。_init_option 建过的那一行会留在
+# 库里成为孤儿,且里面存着明文 SMTP 密码,这里一并删掉。
+
+from django.db import migrations
+
+
+def remove_smtp_config(apps, schema_editor):
+ SysOptions = apps.get_model("options", "SysOptions")
+ SysOptions.objects.filter(key="smtp_config").delete()
+
+
+def restore_smtp_config(apps, schema_editor):
+ SysOptions = apps.get_model("options", "SysOptions")
+ SysOptions.objects.get_or_create(key="smtp_config", defaults={"value": {}})
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("options", "0002_add_sql_language"),
+ ]
+
+ operations = [
+ migrations.RunPython(remove_smtp_config, restore_smtp_config),
+ ]
diff --git a/options/options.py b/options/options.py
index 27e5691..5a42cbc 100644
--- a/options/options.py
+++ b/options/options.py
@@ -102,7 +102,6 @@ class OptionKeys:
allow_register = "allow_register"
submission_list_show_all = "submission_list_show_all"
class_list = "class_list"
- smtp_config = "smtp_config"
judge_server_token = "judge_server_token"
throttling = "throttling"
languages = "languages"
@@ -117,7 +116,6 @@ class OptionDefaultValue:
allow_register = True
submission_list_show_all = True
class_list = []
- smtp_config = {}
judge_server_token = default_token
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50}, "user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
languages = languages
@@ -242,14 +240,6 @@ class _SysOptionsMeta(type):
def class_list(cls, value):
cls._set_option(OptionKeys.class_list, value)
- @my_property
- def smtp_config(cls):
- return cls._get_option(OptionKeys.smtp_config)
-
- @smtp_config.setter
- def smtp_config(cls, value):
- cls._set_option(OptionKeys.smtp_config, value)
-
@my_property(ttl=DEFAULT_SHORT_TTL)
def judge_server_token(cls):
return cls._get_option(OptionKeys.judge_server_token)
diff --git a/utils/shortcuts.py b/utils/shortcuts.py
index aad9f2a..e27bae5 100644
--- a/utils/shortcuts.py
+++ b/utils/shortcuts.py
@@ -2,12 +2,9 @@ import os
import random
import re
from base64 import b64encode
-from email.utils import formataddr
from io import BytesIO
-from django.core.mail import EmailMultiAlternatives, get_connection
from django.utils.crypto import get_random_string
-from django.utils.html import strip_tags
def rand_str(length=32, type="lower_hex"):
@@ -63,25 +60,6 @@ def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
return [int(text) if text.isdigit() else text.lower() for text in re.split(_nsre, s)]
-def send_email(smtp_config, from_name, to_email, to_name, subject, content):
- connection = get_connection(
- host=smtp_config["server"],
- port=smtp_config["port"],
- username=smtp_config["email"],
- password=smtp_config["password"],
- use_tls=smtp_config["tls"],
- )
- message = EmailMultiAlternatives(
- subject=subject,
- body=strip_tags(content),
- from_email=formataddr((from_name, smtp_config["email"])),
- to=[formataddr((to_name, to_email))],
- connection=connection,
- )
- message.attach_alternative(content, "text/html")
- return message.send()
-
-
def get_env(name, default=""):
return os.environ.get(name, default)