diff --git a/account/models.py b/account/models.py index b909f93..2db0e55 100644 --- a/account/models.py +++ b/account/models.py @@ -24,22 +24,22 @@ class UserManager(models.Manager): class User(AbstractBaseUser): - username = models.CharField(max_length=30, unique=True) - email = models.EmailField(max_length=254, null=True) + username = models.CharField(max_length=32, unique=True) + email = models.EmailField(max_length=64, null=True) create_time = models.DateTimeField(auto_now_add=True, null=True) # One of UserType - admin_type = models.CharField(max_length=24, default=AdminType.REGULAR_USER) - problem_permission = models.CharField(max_length=24, default=ProblemPermission.NONE) - reset_password_token = models.CharField(max_length=40, null=True) + admin_type = models.CharField(max_length=32, default=AdminType.REGULAR_USER) + problem_permission = models.CharField(max_length=32, default=ProblemPermission.NONE) + reset_password_token = models.CharField(max_length=32, null=True) reset_password_token_expire_time = models.DateTimeField(null=True) # SSO auth token - auth_token = models.CharField(max_length=40, null=True) + auth_token = models.CharField(max_length=32, null=True) two_factor_auth = models.BooleanField(default=False) - tfa_token = models.CharField(max_length=40, null=True) + tfa_token = models.CharField(max_length=32, null=True) session_keys = JSONField(default=[]) # open api key open_api = models.BooleanField(default=False) - open_api_appkey = models.CharField(max_length=35, null=True) + open_api_appkey = models.CharField(max_length=32, null=True) is_disabled = models.BooleanField(default=False) USERNAME_FIELD = "username" @@ -63,10 +63,6 @@ class User(AbstractBaseUser): db_table = "user" -def _default_avatar(): - return f"/{settings.IMAGE_UPLOAD_DIR}/default.png" - - class UserProfile(models.Model): user = models.OneToOneField(User) # Store user problem solution status with json string format @@ -75,14 +71,13 @@ class UserProfile(models.Model): # {problems: {1: 33}, contest_problems: {1: 44}, record problem_id and score oi_problems_status = JSONField(default={}) - real_name = models.CharField(max_length=30, blank=True, null=True) - avatar = models.CharField(max_length=50, default=_default_avatar()) + real_name = models.CharField(max_length=32, blank=True, null=True) + avatar = models.CharField(max_length=256, default=f"{settings.IMAGE_UPLOAD_DIR}/default.png") blog = models.URLField(blank=True, null=True) - mood = models.CharField(max_length=200, blank=True, null=True) - github = models.CharField(max_length=50, blank=True, null=True) - school = models.CharField(max_length=200, blank=True, null=True) - major = models.CharField(max_length=200, blank=True, null=True) - language = models.CharField(max_length=32, blank=True, null=True) + mood = models.CharField(max_length=256, blank=True, null=True) + github = models.CharField(max_length=64, blank=True, null=True) + school = models.CharField(max_length=64, blank=True, null=True) + major = models.CharField(max_length=64, blank=True, null=True) # for ACM accepted_number = models.IntegerField(default=0) # for OI diff --git a/account/serializers.py b/account/serializers.py index 8345fc6..1b29170 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -6,27 +6,27 @@ from .models import AdminType, ProblemPermission, User, UserProfile class UserLoginSerializer(serializers.Serializer): - username = serializers.CharField(max_length=30) - password = serializers.CharField(max_length=30) - tfa_code = serializers.CharField(min_length=6, max_length=6, required=False, allow_null=True) + username = serializers.CharField() + password = serializers.CharField() + tfa_code = serializers.CharField(required=False, allow_null=True) class UsernameOrEmailCheckSerializer(serializers.Serializer): - username = serializers.CharField(max_length=30, required=False) - email = serializers.EmailField(max_length=30, required=False) + username = serializers.CharField(required=False) + email = serializers.EmailField(required=False) class UserRegisterSerializer(serializers.Serializer): - username = serializers.CharField(max_length=30) - password = serializers.CharField(max_length=30, min_length=6) - email = serializers.EmailField(max_length=30) - captcha = serializers.CharField(max_length=4, min_length=1) + username = serializers.CharField(max_length=32) + password = serializers.CharField(min_length=6) + email = serializers.EmailField(max_length=64) + captcha = serializers.CharField() class UserChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField() - new_password = serializers.CharField(max_length=30, min_length=6) - captcha = serializers.CharField(max_length=4, min_length=4) + new_password = serializers.CharField(min_length=6) + captcha = serializers.CharField() class UserSerializer(serializers.ModelSerializer): @@ -58,9 +58,9 @@ class UserInfoSerializer(serializers.ModelSerializer): class EditUserSerializer(serializers.Serializer): id = serializers.IntegerField() - username = serializers.CharField(max_length=30) - password = serializers.CharField(max_length=30, min_length=6, allow_blank=True, required=False, default=None) - email = serializers.EmailField(max_length=254) + username = serializers.CharField(max_length=32) + password = serializers.CharField(min_length=6, allow_blank=True, required=False, default=None) + email = serializers.EmailField(max_length=64) admin_type = serializers.ChoiceField(choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN)) problem_permission = serializers.ChoiceField(choices=(ProblemPermission.NONE, ProblemPermission.OWN, ProblemPermission.ALL)) @@ -70,29 +70,29 @@ class EditUserSerializer(serializers.Serializer): class EditUserProfileSerializer(serializers.Serializer): - real_name = serializers.CharField(max_length=30, allow_blank=True) - avatar = serializers.CharField(max_length=100, allow_blank=True, required=False) - blog = serializers.URLField(allow_blank=True, required=False) - mood = serializers.CharField(max_length=200, allow_blank=True, required=False) - github = serializers.CharField(max_length=50, allow_blank=True, required=False) - school = serializers.CharField(max_length=200, allow_blank=True, required=False) - major = serializers.CharField(max_length=200, allow_blank=True, required=False) + real_name = serializers.CharField(max_length=32, allow_blank=True) + avatar = serializers.CharField(max_length=256, allow_blank=True, required=False) + blog = serializers.URLField(max_length=256, allow_blank=True, required=False) + mood = serializers.CharField(max_length=256, allow_blank=True, required=False) + github = serializers.CharField(max_length=64, allow_blank=True, required=False) + school = serializers.CharField(max_length=64, allow_blank=True, required=False) + major = serializers.CharField(max_length=64, allow_blank=True, required=False) class ApplyResetPasswordSerializer(serializers.Serializer): email = serializers.EmailField() - captcha = serializers.CharField(max_length=4, min_length=4) + captcha = serializers.CharField() class ResetPasswordSerializer(serializers.Serializer): - token = serializers.CharField(min_length=1, max_length=40) - password = serializers.CharField(min_length=6, max_length=30) - captcha = serializers.CharField(max_length=4, min_length=4) + token = serializers.CharField() + password = serializers.CharField(min_length=6) + captcha = serializers.CharField() class SSOSerializer(serializers.Serializer): - appkey = serializers.CharField(max_length=35) - token = serializers.CharField(max_length=40) + appkey = serializers.CharField() + token = serializers.CharField() class TwoFactorAuthCodeSerializer(serializers.Serializer): diff --git a/account/urls/oj.py b/account/urls/oj.py index b80b34e..cc71683 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -19,7 +19,6 @@ urlpatterns = [ url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), url(r"^upload_avatar/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), - url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"), diff --git a/account/views/oj.py b/account/views/oj.py index aad7c7d..1225556 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -12,11 +12,10 @@ from django.utils.timezone import now from django.views.decorators.csrf import ensure_csrf_cookie from otpauth import OtpAuth +from utils.constants import ContestRuleType from options.options import SysOptions from utils.api import APIView, validate_serializer -from utils.cache import default_cache from utils.captcha import Captcha -from utils.constants import CacheKey from utils.shortcuts import rand_str, img2base64, timestamp2utcstr from ..decorators import login_required from ..models import User, UserProfile @@ -38,7 +37,7 @@ class UserProfileAPI(APIView): """ user = request.user if not user.is_authenticated(): - return self.success({}) + return self.success() username = request.GET.get("username") try: if username: @@ -47,8 +46,7 @@ class UserProfileAPI(APIView): user = request.user except User.DoesNotExist: return self.error("User does not exist") - profile = UserProfile.objects.select_related("user").get(user=user) - return self.success(UserProfileSerializer(profile).data) + return self.success(UserProfileSerializer(user.userprofile).data) @validate_serializer(EditUserProfileSerializer) @login_required @@ -71,8 +69,7 @@ class AvatarUploadAPI(APIView): avatar = form.cleaned_data["file"] else: return self.error("Invalid file content") - # 2097152 = 2 * 1024 * 1024 = 2MB - if avatar.size > 2097152: + if avatar.size > 2 * 1024 * 1024: return self.error("Picture is too large") suffix = os.path.splitext(avatar.name)[-1].lower() if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: @@ -83,46 +80,12 @@ class AvatarUploadAPI(APIView): for chunk in avatar: img.write(chunk) user_profile = request.user.userprofile - _, old_avatar = os.path.split(user_profile.avatar) - if old_avatar != "default.png": - os.remove(os.path.join(settings.IMAGE_UPLOAD_DIR_ABS, old_avatar)) - user_profile.avatar = f"/{settings.IMAGE_UPLOAD_DIR}/{name}" + user_profile.avatar = f"{settings.IMAGE_UPLOAD_DIR}/{name}" user_profile.save() return self.success("Succeeded") -class SSOAPI(APIView): - @login_required - def get(self, request): - callback = request.GET.get("callback", None) - if not callback: - return self.error("Parameter Error") - token = rand_str() - request.user.auth_token = token - request.user.save() - return self.success({"redirect_url": callback + "?token=" + token, - "callback": callback}) - - @validate_serializer(SSOSerializer) - def post(self, request): - data = request.data - try: - User.objects.get(open_api_appkey=data["appkey"]) - except User.DoesNotExist: - return self.error("Invalid appkey") - try: - user = User.objects.get(auth_token=data["token"]) - user.auth_token = None - user.save() - return self.success({"username": user.username, - "id": user.id, - "admin_type": user.admin_type, - "avatar": user.userprofile.avatar}) - except User.DoesNotExist: - return self.error("User does not exist") - - class TwoFactorAuthAPI(APIView): @login_required def get(self, request): @@ -131,7 +94,7 @@ class TwoFactorAuthAPI(APIView): """ user = request.user if user.two_factor_auth: - return self.error("Already open 2FA") + return self.error("2FA is already turned on") token = rand_str() user.tfa_token = token user.save() @@ -161,7 +124,7 @@ class TwoFactorAuthAPI(APIView): code = request.data["code"] user = request.user if not user.two_factor_auth: - return self.error("Other session have disabled TFA") + return self.error("2FA is already turned off") if OtpAuth(user.tfa_token).valid_totp(code): user.two_factor_auth = False user.save() @@ -198,7 +161,7 @@ class UserLoginAPI(APIView): # None is returned if username or password is wrong if user: if user.is_disabled: - return self.error("Your account have been disabled") + return self.error("Your account has been disabled") if not user.two_factor_auth: auth.login(request, user) return self.success("Succeeded") @@ -218,13 +181,13 @@ class UserLoginAPI(APIView): # todo remove this, only for debug use def get(self, request): auth.login(request, auth.authenticate(username=request.GET["username"], password=request.GET["password"])) - return self.success({}) + return self.success() class UserLogoutAPI(APIView): def get(self, request): auth.logout(request) - return self.success({}) + return self.success() class UsernameOrEmailCheck(APIView): @@ -240,11 +203,9 @@ class UsernameOrEmailCheck(APIView): "email": False } if data.get("username"): - if User.objects.filter(username=data["username"]).exists(): - result["username"] = True + result["username"] = User.objects.filter(username=data["username"]).exists() if data.get("email"): - if User.objects.filter(email=data["email"]).exists(): - result["email"] = True + result["email"] = User.objects.filter(email=data["email"]).exists() return self.success(result) @@ -254,17 +215,9 @@ class UserRegisterAPI(APIView): """ User register api """ - config = default_cache.get(CacheKey.website_config) - if config: - config = pickle.loads(config) - else: - config = WebsiteConfig.objects.first() - if not config: - config = WebsiteConfig.objects.create() - default_cache.set(CacheKey.website_config, pickle.dumps(config)) - if not config.allow_register: - return self.error("Register have been disabled by admin") + if not SysOptions.allow_register: + return self.error("Register function has been disabled by admin") data = request.data captcha = Captcha(request) @@ -293,6 +246,7 @@ class UserChangePasswordAPI(APIView): username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) if user: + # TODO: check tfa? user.set_password(data["new_password"]) user.save() return self.success("Succeeded") @@ -305,7 +259,6 @@ class ApplyResetPasswordAPI(APIView): def post(self, request): data = request.data captcha = Captcha(request) - config = WebsiteConfig.objects.first() if not captcha.check(data["captcha"]): return self.error("Invalid captcha") try: @@ -320,14 +273,14 @@ class ApplyResetPasswordAPI(APIView): user.save() render_data = { "username": user.username, - "website_name": config.name, - "link": f"{config.base_url}/reset-password/{user.reset_password_token}" + "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.delay(config.name, + send_email_async.delay(SysOptions.website_name, user.email, user.username, - config.name + " 登录信息找回邮件", + f"{SysOptions.website_name} 登录信息找回邮件", email_html) return self.success("Succeeded") @@ -342,9 +295,9 @@ class ResetPasswordAPI(APIView): try: user = User.objects.get(reset_password_token=data["token"]) except User.DoesNotExist: - return self.error("Token dose not exist") - if int((user.reset_password_token_expire_time - now()).total_seconds()) < 0: - return self.error("Token have expired") + 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.two_factor_auth = False user.set_password(data["password"]) @@ -356,13 +309,13 @@ class SessionManagementAPI(APIView): @login_required def get(self, request): engine = import_module(settings.SESSION_ENGINE) - SessionStore = engine.SessionStore + session_store = engine.SessionStore current_session = request.session.session_key session_keys = request.user.session_keys result = [] modified = False for key in session_keys[:]: - session = SessionStore(key) + session = session_store(key) # session does not exist or is expiry if not session._session: session_keys.remove(key) @@ -398,12 +351,12 @@ class SessionManagementAPI(APIView): class UserRankAPI(APIView): def get(self, request): rule_type = request.GET.get("rule") - if rule_type not in ["acm", "oi"]: - rule_type = "acm" + if rule_type not in ContestRuleType.choices(): + rule_type = ContestRuleType.ACM profiles = UserProfile.objects.select_related("user")\ .filter(submission_number__gt=0)\ .exclude(user__is_disabled=True) - if rule_type == "acm": + if rule_type == ContestRuleType.ACM: profiles = profiles.order_by("-accepted_number", "submission_number") else: profiles = profiles.order_by("-total_score") diff --git a/announcement/models.py b/announcement/models.py index 186d4ea..49f57b8 100644 --- a/announcement/models.py +++ b/announcement/models.py @@ -5,7 +5,7 @@ from utils.models import RichTextField class Announcement(models.Model): - title = models.CharField(max_length=50) + title = models.CharField(max_length=64) # HTML content = RichTextField() create_time = models.DateTimeField(auto_now_add=True) diff --git a/announcement/serializers.py b/announcement/serializers.py index 0c0becc..b660a61 100644 --- a/announcement/serializers.py +++ b/announcement/serializers.py @@ -5,8 +5,8 @@ from .models import Announcement class CreateAnnouncementSerializer(serializers.Serializer): - title = serializers.CharField(max_length=50) - content = serializers.CharField(max_length=10000) + title = serializers.CharField(max_length=64) + content = serializers.CharField(max_length=1024 * 1024 * 8) visible = serializers.BooleanField() @@ -21,6 +21,6 @@ class AnnouncementSerializer(serializers.ModelSerializer): class EditAnnouncementSerializer(serializers.Serializer): id = serializers.IntegerField() - title = serializers.CharField(max_length=50) - content = serializers.CharField(max_length=10000) + title = serializers.CharField(max_length=64) + content = serializers.CharField(max_length=1024 * 1024 * 8) visible = serializers.BooleanField() diff --git a/conf/models.py b/conf/models.py index 86248db..4c6348d 100644 --- a/conf/models.py +++ b/conf/models.py @@ -3,16 +3,16 @@ from django.utils import timezone class JudgeServer(models.Model): - hostname = models.CharField(max_length=64) + hostname = models.CharField(max_length=128) ip = models.CharField(max_length=32, blank=True, null=True) - judger_version = models.CharField(max_length=24) + judger_version = models.CharField(max_length=32) cpu_core = models.IntegerField() memory_usage = models.FloatField() cpu_usage = models.FloatField() last_heartbeat = models.DateTimeField() create_time = models.DateTimeField(auto_now_add=True) task_number = models.IntegerField(default=0) - service_url = models.CharField(max_length=128, blank=True, null=True) + service_url = models.CharField(max_length=256, blank=True, null=True) @property def status(self): diff --git a/conf/serializers.py b/conf/serializers.py index 09f9940..7f0cf57 100644 --- a/conf/serializers.py +++ b/conf/serializers.py @@ -21,9 +21,9 @@ class TestSMTPConfigSerializer(serializers.Serializer): class CreateEditWebsiteConfigSerializer(serializers.Serializer): website_base_url = serializers.CharField(max_length=128) - website_name = serializers.CharField(max_length=32) - website_name_shortcut = serializers.CharField(max_length=32) - website_footer = serializers.CharField(max_length=1024) + website_name = serializers.CharField(max_length=64) + website_name_shortcut = serializers.CharField(max_length=64) + website_footer = serializers.CharField(max_length=1024 * 1024) allow_register = serializers.BooleanField() submission_list_show_all = serializers.BooleanField() @@ -39,10 +39,10 @@ class JudgeServerSerializer(serializers.ModelSerializer): class JudgeServerHeartbeatSerializer(serializers.Serializer): - hostname = serializers.CharField(max_length=64) - judger_version = serializers.CharField(max_length=24) + hostname = serializers.CharField(max_length=128) + judger_version = serializers.CharField(max_length=32) cpu_core = serializers.IntegerField(min_value=1) memory = serializers.FloatField(min_value=0, max_value=100) cpu = serializers.FloatField(min_value=0, max_value=100) action = serializers.ChoiceField(choices=("heartbeat", )) - service_url = serializers.CharField(max_length=128, required=False) + service_url = serializers.CharField(max_length=256, required=False) diff --git a/contest/models.py b/contest/models.py index 38b2356..d66d400 100644 --- a/contest/models.py +++ b/contest/models.py @@ -2,26 +2,11 @@ from django.db import models from django.utils.timezone import now from jsonfield import JSONField +from utils.constants import ContestStatus, ContestRuleType, ContestType from account.models import User, AdminType from utils.models import RichTextField -class ContestType(object): - PUBLIC_CONTEST = "Public" - PASSWORD_PROTECTED_CONTEST = "Password Protected" - - -class ContestStatus(object): - CONTEST_NOT_START = "1" - CONTEST_ENDED = "-1" - CONTEST_UNDERWAY = "0" - - -class ContestRuleType(object): - ACM = "ACM" - OI = "OI" - - class Contest(models.Model): title = models.CharField(max_length=40) description = RichTextField() diff --git a/contest/views/oj.py b/contest/views/oj.py index a319513..c4fd4fc 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,13 +1,11 @@ -import pickle from django.utils.timezone import now from django.core.cache import cache from utils.api import APIView, validate_serializer -from utils.cache import default_cache from utils.constants import CacheKey from account.decorators import login_required, check_contest_permission -from ..models import ContestAnnouncement, Contest, ContestStatus, ContestRuleType -from ..models import OIContestRank, ACMContestRank +from utils.constants import ContestRuleType, ContestType, ContestStatus +from ..models import ContestAnnouncement, Contest, OIContestRank, ACMContestRank from ..serializers import ContestAnnouncementSerializer from ..serializers import ContestSerializer, ContestPasswordVerifySerializer from ..serializers import OIContestRankSerializer, ACMContestRankSerializer diff --git a/utils/constants.py b/utils/constants.py index 14f13af..be7057a 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -1,3 +1,26 @@ +class Choices: + @classmethod + def choices(cls): + d = cls.__dict__ + return [d[item] for item in d.keys() if not item.startswith("__")] + + +class ContestType: + PUBLIC_CONTEST = "Public" + PASSWORD_PROTECTED_CONTEST = "Password Protected" + + +class ContestStatus: + CONTEST_NOT_START = "1" + CONTEST_ENDED = "-1" + CONTEST_UNDERWAY = "0" + + +class ContestRuleType(Choices): + ACM = "ACM" + OI = "OI" + + class CacheKey: waiting_queue = "waiting_queue" contest_rank_cache = "contest_rank_cache_" diff --git a/utils/xss_filter.py b/utils/xss_filter.py index d29495b..34d65a8 100644 --- a/utils/xss_filter.py +++ b/utils/xss_filter.py @@ -26,11 +26,8 @@ Cannot defense xss in browser which is belowed IE7 浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS """ import re - -try: - from html.parser import HTMLParser -except: - from HTMLParser import HTMLParser +import copy +from html.parser import HTMLParser class XssHtml(HTMLParser): @@ -163,7 +160,7 @@ class XssHtml(HTMLParser): else: other = [] if attrs: - for (key, value) in attrs.items(): + for key, value in copy.deepcopy(attrs).items(): if key not in self.common_attrs + other: del attrs[key] return attrs