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)