Files
OnlineJudge/account/models.py
yuetsh 989b33d827 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>
2026-08-05 23:00:45 -06:00

131 lines
4.4 KiB
Python

from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from utils.models import JSONField
class AdminType(models.TextChoices):
REGULAR_USER = "Regular User", "Regular User"
STUDENT_ADMIN = "Student Admin", "Student Admin"
TEACHER_ADMIN = "Teacher Admin", "Teacher Admin"
SUPER_ADMIN = "Super Admin", "Super Admin"
class ProblemPermission(models.TextChoices):
NONE = "None", "None"
OWN = "Own", "Own"
ALL = "All", "All"
class UserManager(models.Manager):
use_in_migrations = True
def get_by_natural_key(self, username):
return self.get(**{f"{self.model.USERNAME_FIELD}__iexact": username})
async def aget_by_natural_key(self, username):
return await self.aget(**{f"{self.model.USERNAME_FIELD}__iexact": username})
class User(AbstractBaseUser):
username = models.TextField(unique=True)
class_name = models.TextField(null=True)
email = models.TextField(null=True)
create_time = models.DateTimeField(auto_now_add=True, null=True)
# 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()))
# open api key
open_api = models.BooleanField(default=False, db_default=False)
open_api_appkey = models.TextField(null=True)
is_disabled = models.BooleanField(default=False, db_default=False)
raw_password = models.CharField(max_length=20, null=True, blank=True, verbose_name="明文密码")
USERNAME_FIELD = "username"
REQUIRED_FIELDS = []
objects = UserManager()
def is_regular_user(self):
return self.admin_type == AdminType.REGULAR_USER
def is_student_admin(self):
return self.admin_type == AdminType.STUDENT_ADMIN
def is_teacher_admin(self):
return self.admin_type == AdminType.TEACHER_ADMIN
def is_super_admin(self):
return self.admin_type == AdminType.SUPER_ADMIN
def is_admin_role(self):
return self.admin_type in [
AdminType.STUDENT_ADMIN,
AdminType.TEACHER_ADMIN,
AdminType.SUPER_ADMIN,
]
def is_teacher_or_above(self):
return self.admin_type in [AdminType.TEACHER_ADMIN, AdminType.SUPER_ADMIN]
def can_mgmt_all_problem(self):
return self.problem_permission == ProblemPermission.ALL
def is_contest_admin(self, contest):
return self.is_authenticated and (contest.created_by == self or self.admin_type == AdminType.SUPER_ADMIN)
def set_password(self, raw_password):
super().set_password(raw_password)
self.raw_password = raw_password
class Meta:
db_table = "user"
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
# acm_problems_status examples:
# {
# "problems": {
# "1": {
# "status": JudgeStatus.ACCEPTED,
# "_id": "1000"
# }
# },
# "contest_problems": {
# "1": {
# "status": JudgeStatus.ACCEPTED,
# "_id": "1000"
# }
# }
# }
acm_problems_status = JSONField(default=dict, db_default=models.Value({}, output_field=models.JSONField()))
real_name = models.TextField(null=True)
avatar = models.TextField(default=f"{settings.AVATAR_URI_PREFIX}/default.png")
blog = models.URLField(null=True)
mood = models.TextField(null=True)
github = models.TextField(null=True)
school = models.TextField(null=True)
major = models.TextField(null=True)
language = models.TextField(null=True)
accepted_number = models.IntegerField(default=0, db_default=0)
submission_number = models.IntegerField(default=0, db_default=0)
def add_accepted_problem_number(self):
self.accepted_number = models.F("accepted_number") + 1
self.save(update_fields=["accepted_number"])
def add_submission_number(self):
self.submission_number = models.F("submission_number") + 1
self.save(update_fields=["submission_number"])
class Meta:
db_table = "user_profile"