refactor: 移除 2FA 功能与三个冗余依赖
2FA 早已是死代码:TOTP 相关端点标注为前端未调用,前端唯一的 two_factor_auth 是硬编码 false,没有任何入口能打开它。 - 删除 TwoFactorAuthAPI、CheckTFARequiredAPI、_totp* 辅助函数、 TwoFactorAuthCodeSerializer 及各 serializer 的 tfa_code 字段 - 删除管理端写 two_factor_auth/tfa_token 的分支。登录已不再校验 TOTP,若保留写入路径,置 True 会变成静默的安全降级 - 删除 User.two_factor_auth / tfa_token 字段(迁移 0009) - 移除 django-dbconn-retry:仅挂在 INSTALLED_APPS,代码零引用。 DATABASES 未配置 CONN_MAX_AGE(默认 0),本就没有持久连接需要 重连修复。要开持久连接用 Django 自带的 CONN_HEALTH_CHECKS - requests 换成 httpx(openai 已经引入):删除废弃的 ReleaseNotesAPI,judge dispatcher 显式传 timeout=None 以保持 requests 原本的无超时行为,判题请求是同步等结果的 依赖净减 5 个:otpauth、qrcode、requests、charset-normalizer、 django-dbconn-retry Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 6.0.4 on 2026-08-06 04:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0008_remove_userprofile_oi_problems_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='tfa_token',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='two_factor_auth',
|
||||
),
|
||||
]
|
||||
@@ -40,8 +40,6 @@ class User(AbstractBaseUser):
|
||||
reset_password_token_expire_time = models.DateTimeField(null=True)
|
||||
# SSO auth token
|
||||
auth_token = models.TextField(null=True)
|
||||
two_factor_auth = models.BooleanField(default=False, db_default=False)
|
||||
tfa_token = models.TextField(null=True)
|
||||
session_keys = JSONField(default=list, db_default=models.Value([], output_field=models.JSONField()))
|
||||
# open api key
|
||||
open_api = models.BooleanField(default=False, db_default=False)
|
||||
|
||||
@@ -8,7 +8,6 @@ from .models import AdminType, ProblemPermission, User, UserProfile
|
||||
class UserLoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField()
|
||||
tfa_code = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
class UsernameOrEmailCheckSerializer(serializers.Serializer):
|
||||
@@ -26,13 +25,11 @@ class UserRegisterSerializer(serializers.Serializer):
|
||||
class UserChangePasswordSerializer(serializers.Serializer):
|
||||
old_password = serializers.CharField()
|
||||
new_password = serializers.CharField(min_length=6)
|
||||
tfa_code = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
class UserChangeEmailSerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
new_email = serializers.EmailField(max_length=64)
|
||||
tfa_code = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
class GenerateUserSerializer(serializers.Serializer):
|
||||
@@ -61,7 +58,6 @@ class UserAdminSerializer(serializers.ModelSerializer):
|
||||
"real_name",
|
||||
"create_time",
|
||||
"last_login",
|
||||
"two_factor_auth",
|
||||
"open_api",
|
||||
"is_disabled",
|
||||
"raw_password",
|
||||
@@ -89,7 +85,6 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
"problem_permission",
|
||||
"create_time",
|
||||
"last_login",
|
||||
"two_factor_auth",
|
||||
"open_api",
|
||||
"is_disabled",
|
||||
"class_name",
|
||||
@@ -121,7 +116,6 @@ class EditUserSerializer(serializers.Serializer):
|
||||
admin_type = serializers.ChoiceField(choices=AdminType.choices)
|
||||
problem_permission = serializers.ChoiceField(choices=ProblemPermission.choices)
|
||||
open_api = serializers.BooleanField()
|
||||
two_factor_auth = serializers.BooleanField()
|
||||
is_disabled = serializers.BooleanField()
|
||||
class_name = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
|
||||
@@ -152,10 +146,6 @@ class SSOSerializer(serializers.Serializer):
|
||||
token = serializers.CharField()
|
||||
|
||||
|
||||
class TwoFactorAuthCodeSerializer(serializers.Serializer):
|
||||
code = serializers.IntegerField()
|
||||
|
||||
|
||||
class ImageUploadForm(forms.Form):
|
||||
image = forms.FileField()
|
||||
|
||||
|
||||
@@ -6,13 +6,11 @@ from ..views.oj import (
|
||||
SSOAPI,
|
||||
ApplyResetPasswordAPI,
|
||||
AvatarUploadAPI,
|
||||
CheckTFARequiredAPI,
|
||||
Metrics,
|
||||
OpenAPIAppkeyAPI,
|
||||
ProfileProblemDisplayIDRefreshAPI,
|
||||
ResetPasswordAPI,
|
||||
SessionManagementAPI,
|
||||
TwoFactorAuthAPI,
|
||||
UserActivityRankAPI,
|
||||
UserChangeEmailAPI,
|
||||
UserChangePasswordAPI,
|
||||
@@ -39,11 +37,6 @@ urlpatterns = [
|
||||
path("profile/fresh_display_id", ProfileProblemDisplayIDRefreshAPI.as_view()),
|
||||
path("metrics", Metrics.as_view()),
|
||||
path("upload_avatar", AvatarUploadAPI.as_view()),
|
||||
path("tfa_required", CheckTFARequiredAPI.as_view()), # DEPRECATED: 前端未调用
|
||||
path(
|
||||
"two_factor_auth", # DEPRECATED: 前端未调用
|
||||
TwoFactorAuthAPI.as_view(),
|
||||
),
|
||||
path("user_rank", UserRankAPI.as_view()),
|
||||
path("user_activity_rank", UserActivityRankAPI.as_view()),
|
||||
path("user_problem_rank", UserProblemRankAPI.as_view()),
|
||||
|
||||
@@ -112,15 +112,6 @@ class UserAdminAPI(APIView):
|
||||
user.open_api_appkey = None
|
||||
user.open_api = data["open_api"]
|
||||
|
||||
if data["two_factor_auth"]:
|
||||
# Avoid reset user tfa_token after saving changes
|
||||
if not user.two_factor_auth:
|
||||
user.tfa_token = rand_str()
|
||||
else:
|
||||
user.tfa_token = None
|
||||
|
||||
user.two_factor_auth = data["two_factor_auth"]
|
||||
|
||||
user.save()
|
||||
if pre_username != user.username:
|
||||
Submission.objects.filter(username=pre_username).update(username=user.username)
|
||||
|
||||
@@ -3,7 +3,6 @@ import os
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
import qrcode
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.db.models import Count, Q
|
||||
@@ -12,7 +11,6 @@ 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 otpauth import TOTP
|
||||
|
||||
from options.options import SysOptions
|
||||
from problem.models import Problem
|
||||
@@ -21,7 +19,7 @@ from utils.api import APIView, AsyncAPIView, CSRFExemptAPIView, validate_seriali
|
||||
from utils.async_helpers import async_cache_get, async_cache_set
|
||||
from utils.captcha import Captcha
|
||||
from utils.constants import CacheKey
|
||||
from utils.shortcuts import datetime2str, img2base64, rand_str
|
||||
from utils.shortcuts import datetime2str, rand_str
|
||||
|
||||
from ..decorators import login_required
|
||||
from ..models import AdminType, User, UserProfile
|
||||
@@ -32,7 +30,6 @@ from ..serializers import (
|
||||
RankInfoSerializer,
|
||||
ResetPasswordSerializer,
|
||||
SSOSerializer,
|
||||
TwoFactorAuthCodeSerializer,
|
||||
UserChangeEmailSerializer,
|
||||
UserChangePasswordSerializer,
|
||||
UserLoginSerializer,
|
||||
@@ -43,22 +40,6 @@ from ..serializers import (
|
||||
from ..tasks import send_email_async
|
||||
|
||||
|
||||
def _totp(token):
|
||||
return TOTP(token.encode("utf-8"))
|
||||
|
||||
|
||||
def _totp_uri(token, label, issuer):
|
||||
return _totp(token).to_uri(label, issuer)
|
||||
|
||||
|
||||
def _valid_totp(token, code):
|
||||
try:
|
||||
code = int(code)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return _totp(token).verify(code)
|
||||
|
||||
|
||||
class UserProfileAPI(AsyncAPIView):
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
async def get(self, request, **kwargs):
|
||||
@@ -136,72 +117,6 @@ class AvatarUploadAPI(AsyncAPIView):
|
||||
return self.success("Succeeded")
|
||||
|
||||
|
||||
# DEPRECATED: 前端未调用 (2026-05-26)
|
||||
class TwoFactorAuthAPI(APIView):
|
||||
@login_required
|
||||
def get(self, request):
|
||||
"""
|
||||
Get QR code
|
||||
"""
|
||||
user = request.user
|
||||
if user.two_factor_auth:
|
||||
return self.error("2FA is already turned on")
|
||||
token = rand_str()
|
||||
user.tfa_token = token
|
||||
user.save()
|
||||
|
||||
label = f"{SysOptions.website_name_shortcut}:{user.username}"
|
||||
image = qrcode.make(_totp_uri(token, label, SysOptions.website_name.replace(" ", "")))
|
||||
return self.success(img2base64(image))
|
||||
|
||||
@login_required
|
||||
@validate_serializer(TwoFactorAuthCodeSerializer)
|
||||
def post(self, request):
|
||||
"""
|
||||
Open 2FA
|
||||
"""
|
||||
code = request.data["code"]
|
||||
user = request.user
|
||||
if _valid_totp(user.tfa_token, code):
|
||||
user.two_factor_auth = True
|
||||
user.save()
|
||||
return self.success("Succeeded")
|
||||
else:
|
||||
return self.error("Invalid code")
|
||||
|
||||
@login_required
|
||||
@validate_serializer(TwoFactorAuthCodeSerializer)
|
||||
def put(self, request):
|
||||
code = request.data["code"]
|
||||
user = request.user
|
||||
if not user.two_factor_auth:
|
||||
return self.error("2FA is already turned off")
|
||||
if _valid_totp(user.tfa_token, code):
|
||||
user.two_factor_auth = False
|
||||
user.save()
|
||||
return self.success("Succeeded")
|
||||
else:
|
||||
return self.error("Invalid code")
|
||||
|
||||
|
||||
# DEPRECATED: 前端未调用 (2026-05-26)
|
||||
class CheckTFARequiredAPI(APIView):
|
||||
@validate_serializer(UsernameOrEmailCheckSerializer)
|
||||
def post(self, request):
|
||||
"""
|
||||
Check TFA is required
|
||||
"""
|
||||
data = request.data
|
||||
result = False
|
||||
if data.get("username"):
|
||||
try:
|
||||
user = User.objects.get(username=data["username"])
|
||||
result = user.two_factor_auth
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
return self.success({"result": result})
|
||||
|
||||
|
||||
class UserLoginAPI(AsyncAPIView):
|
||||
@validate_serializer(UserLoginSerializer)
|
||||
async def post(self, request):
|
||||
@@ -210,22 +125,10 @@ class UserLoginAPI(AsyncAPIView):
|
||||
if user:
|
||||
if user.is_disabled:
|
||||
return self.error("Your account has been disabled")
|
||||
if not user.two_factor_auth:
|
||||
prev_login = user.last_login
|
||||
await auth.alogin(request, user)
|
||||
request.session["prev_login"] = datetime2str(prev_login) if prev_login else ""
|
||||
return self.success("Succeeded")
|
||||
|
||||
if user.two_factor_auth and "tfa_code" not in data:
|
||||
return self.error("tfa_required")
|
||||
|
||||
if _valid_totp(user.tfa_token, data["tfa_code"]):
|
||||
prev_login = user.last_login
|
||||
await auth.alogin(request, user)
|
||||
request.session["prev_login"] = datetime2str(prev_login) if prev_login else ""
|
||||
return self.success("Succeeded")
|
||||
else:
|
||||
return self.error("Invalid two factor verification code")
|
||||
prev_login = user.last_login
|
||||
await auth.alogin(request, user)
|
||||
request.session["prev_login"] = datetime2str(prev_login) if prev_login else ""
|
||||
return self.success("Succeeded")
|
||||
else:
|
||||
return self.error("Invalid username or password")
|
||||
|
||||
@@ -284,11 +187,6 @@ class UserChangeEmailAPI(APIView):
|
||||
data = request.data
|
||||
user = auth.authenticate(username=request.user.username, password=data["password"])
|
||||
if user:
|
||||
if user.two_factor_auth:
|
||||
if "tfa_code" not in data:
|
||||
return self.error("tfa_required")
|
||||
if not _valid_totp(user.tfa_token, data["tfa_code"]):
|
||||
return self.error("Invalid two factor verification code")
|
||||
data["new_email"] = data["new_email"].lower()
|
||||
if User.objects.filter(email=data["new_email"]).exists():
|
||||
return self.error("The email is owned by other account")
|
||||
@@ -311,11 +209,6 @@ class UserChangePasswordAPI(APIView):
|
||||
username = request.user.username
|
||||
user = auth.authenticate(username=username, password=data["old_password"])
|
||||
if user:
|
||||
if user.two_factor_auth:
|
||||
if "tfa_code" not in data:
|
||||
return self.error("tfa_required")
|
||||
if not _valid_totp(user.tfa_token, data["tfa_code"]):
|
||||
return self.error("Invalid two factor verification code")
|
||||
user.set_password(data["new_password"])
|
||||
user.save()
|
||||
return self.success("Succeeded")
|
||||
@@ -373,7 +266,6 @@ class ResetPasswordAPI(APIView):
|
||||
if user.reset_password_token_expire_time < now():
|
||||
return self.error("Token has expired")
|
||||
user.reset_password_token = None
|
||||
user.two_factor_auth = False
|
||||
user.set_password(data["password"])
|
||||
user.save()
|
||||
return self.success("Succeeded")
|
||||
|
||||
Reference in New Issue
Block a user