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>
131 lines
4.4 KiB
Python
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"
|