diff --git a/.gitignore b/.gitignore index e087361..8707498 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ db.db #redis dump *.rdb #*.out -db.sqlite3 +*.sqlite3 .DS_Store log/ static/release/css diff --git a/account/migrations/0016_auto_20151211_2230.py b/account/migrations/0016_auto_20151211_2230.py new file mode 100644 index 0000000..9c2f879 --- /dev/null +++ b/account/migrations/0016_auto_20151211_2230.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-11 14:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0015_userprofile_student_id'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='tfa_token', + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name='user', + name='two_factor_auth', + field=models.BooleanField(default=False), + ), + ] diff --git a/account/migrations/0017_auto_20151212_2139.py b/account/migrations/0017_auto_20151212_2139.py new file mode 100644 index 0000000..a34d2fa --- /dev/null +++ b/account/migrations/0017_auto_20151212_2139.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-12 13:39 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0016_auto_20151211_2230'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='tfa_token', + field=models.CharField(blank=True, max_length=40, null=True), + ), + ] diff --git a/account/models.py b/account/models.py index b99c691..65d8745 100644 --- a/account/models.py +++ b/account/models.py @@ -40,6 +40,9 @@ class User(AbstractBaseUser): reset_password_token_create_time = models.DateTimeField(blank=True, null=True) # 论坛授权token auth_token = models.CharField(max_length=40, blank=True, null=True) + # 是否开启两步验证 + two_factor_auth = models.BooleanField(default=False) + tfa_token = models.CharField(max_length=40, blank=True, null=True) USERNAME_FIELD = 'username' REQUIRED_FIELDS = [] diff --git a/account/serializers.py b/account/serializers.py index 0d92ae7..a2d2768 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -7,7 +7,7 @@ from .models import User, UserProfile class UserLoginSerializer(serializers.Serializer): username = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30) - captcha = serializers.CharField(min_length=4, max_length=4) + tfa_code = serializers.CharField(min_length=6, max_length=6, required=False) class UsernameCheckSerializer(serializers.Serializer): @@ -84,3 +84,7 @@ class UserProfileSerializer(serializers.ModelSerializer): model = UserProfile fields = ["avatar", "blog", "mood", "hduoj_username", "bestcoder_username", "codeforces_username", "rank", "accepted_number", "submissions_number", "problems_status", "phone_number", "school", "student_id"] + + +class TwoFactorAuthCodeSerializer(serializers.Serializer): + code = serializers.IntegerField() diff --git a/account/tasks.py b/account/tasks.py new file mode 100644 index 0000000..58cb8fc --- /dev/null +++ b/account/tasks.py @@ -0,0 +1,8 @@ +# coding=utf-8 +from celery import shared_task +from utils.mail import send_email + + +@shared_task +def _send_email(from_name, to_email, to_name, subject, content): + send_email(from_name, to_email, to_name, subject, content) \ No newline at end of file diff --git a/account/views.py b/account/views.py index 5ca7612..9c41b34 100644 --- a/account/views.py +++ b/account/views.py @@ -1,11 +1,13 @@ # coding=utf-8 import codecs +import qrcode +import StringIO from django import http from django.contrib import auth from django.shortcuts import render from django.db.models import Q from django.conf import settings -from django.http import HttpResponseRedirect +from django.http import HttpResponse from django.core.exceptions import MultipleObjectsReturned from django.utils.timezone import now @@ -14,7 +16,9 @@ from rest_framework.response import Response from utils.shortcuts import (serializer_invalid_response, error_response, success_response, error_page, paginate, rand_str) from utils.captcha import Captcha -from utils.mail import send_email +from utils.otp_auth import OtpAuth + +from .tasks import _send_email from .decorators import login_required from .models import User, UserProfile @@ -23,7 +27,8 @@ from .serializers import (UserLoginSerializer, UserRegisterSerializer, UserChangePasswordSerializer, UserSerializer, EditUserSerializer, ApplyResetPasswordSerializer, ResetPasswordSerializer, - SSOSerializer, EditUserProfileSerializer, UserProfileSerializer) + SSOSerializer, EditUserProfileSerializer, + UserProfileSerializer, TwoFactorAuthCodeSerializer) from .decorators import super_admin_required @@ -38,14 +43,23 @@ class UserLoginAPIView(APIView): serializer = UserLoginSerializer(data=request.data) if serializer.is_valid(): data = serializer.data - captcha = Captcha(request) - if not captcha.check(data["captcha"]): - return error_response(u"验证码错误") + print data user = auth.authenticate(username=data["username"], password=data["password"]) # 用户名或密码错误的话 返回None if user: - auth.login(request, user) - return success_response(u"登录成功") + if not user.two_factor_auth: + auth.login(request, user) + return success_response(u"登录成功") + + # 没有输入两步验证的验证码 + if user.two_factor_auth and "tfa_code" not in data: + return success_response("tfa_required") + + if OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]): + auth.login(request, user) + return success_response(u"登录成功") + else: + return error_response(u"验证码错误") else: return error_response(u"用户名或密码错误") else: @@ -63,7 +77,7 @@ def index_page(request): return render(request, "oj/index.html") if request.META.get('HTTP_REFERER') or request.GET.get("index"): - return render(request, "oj/index.html") + return render(request, "oj/index.html") else: return http.HttpResponseRedirect('/problems/') @@ -151,9 +165,9 @@ class EmailCheckAPIView(APIView): 检测邮箱是否存在,用状态码标识结果 --- """ - #这里是为了适应前端表单验证空间的要求 + # 这里是为了适应前端表单验证空间的要求 reset = request.GET.get("reset", None) - #如果reset为true说明该请求是重置密码页面发出的,要返回的状态码应正好相反 + # 如果reset为true说明该请求是重置密码页面发出的,要返回的状态码应正好相反 if reset: existed = 200 does_not_existed = 400 @@ -287,22 +301,25 @@ class ApplyResetPasswordAPIView(APIView): user = User.objects.get(email=data["email"]) except User.DoesNotExist: return error_response(u"用户不存在") - if user.reset_password_token_create_time and (now() - user.reset_password_token_create_time).total_seconds() < 20 * 60: + if user.reset_password_token_create_time and ( + now() - user.reset_password_token_create_time).total_seconds() < 20 * 60: return error_response(u"20分钟内只能找回一次密码") user.reset_password_token = rand_str() user.reset_password_token_create_time = now() user.save() - email_template = codecs.open(settings.TEMPLATES[0]["DIRS"][0] + "utils/reset_password_email.html", "r", "utf-8").read() + email_template = codecs.open(settings.TEMPLATES[0]["DIRS"][0] + "utils/reset_password_email.html", "r", + "utf-8").read() - email_template = email_template.replace("{{ username }}", user.username).\ - replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]).\ - replace("{{ link }}", request.scheme + "://" + request.META['HTTP_HOST'] + "/reset_password/t/" + user.reset_password_token) + email_template = email_template.replace("{{ username }}", user.username). \ + replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]). \ + replace("{{ link }}", request.scheme + "://" + request.META[ + 'HTTP_HOST'] + "/reset_password/t/" + user.reset_password_token) - send_email(settings.WEBSITE_INFO["website_name"], - user.email, - user.username, - settings.WEBSITE_INFO["website_name"] + u" 登录信息找回邮件", - email_template) + _send_email.delay(settings.WEBSITE_INFO["website_name"], + user.email, + user.username, + settings.WEBSITE_INFO["website_name"] + u" 登录信息找回邮件", + email_template) return success_response(u"邮件发送成功,请前往您的邮箱查收") else: return serializer_invalid_response(serializer) @@ -350,7 +367,10 @@ class SSOAPIView(APIView): if serializer.is_valid(): try: user = User.objects.get(auth_token=serializer.data["token"]) - return success_response({"username": user.username}) + user.auth_token = None + user.save() + return success_response( + {"username": user.username, "admin_type": user.admin_type, "avatar": user.userprofile.avatar}) except User.DoesNotExist: return error_response(u"用户不存在") else: @@ -364,7 +384,8 @@ class SSOAPIView(APIView): token = rand_str() request.user.auth_token = token request.user.save() - return render(request, "oj/account/sso.html", {"redirect_url": callback + "?token=" + token, "callback": callback}) + return render(request, "oj/account/sso.html", + {"redirect_url": callback + "?token=" + token, "callback": callback}) def reset_password_page(request, token): @@ -375,3 +396,55 @@ def reset_password_page(request, token): if (now() - user.reset_password_token_create_time).total_seconds() > 30 * 60: return error_page(request, u"链接已过期") return render(request, "oj/account/reset_password.html", {"user": user}) + + +class TwoFactorAuthAPIView(APIView): + @login_required + def get(self, request): + """ + 获取绑定二维码 + """ + user = request.user + if user.two_factor_auth: + return error_response(u"已经开启两步验证了") + token = rand_str() + user.tfa_token = token + user.save() + + image = qrcode.make(OtpAuth(token).to_uri("totp", settings.WEBSITE_INFO["url"], "OnlineJudgeAdmin")) + buf = StringIO.StringIO() + image.save(buf, 'gif') + + return HttpResponse(buf.getvalue(), 'image/gif') + + @login_required + def post(self, request): + """ + 开启两步验证 + """ + serializer = TwoFactorAuthCodeSerializer(data=request.data) + if serializer.is_valid(): + code = serializer.data["code"] + user = request.user + if OtpAuth(user.tfa_token).valid_totp(code): + user.two_factor_auth = True + user.save() + return success_response(u"开启两步验证成功") + else: + return error_response(u"验证码错误") + else: + return serializer_invalid_response(serializer) + + @login_required + def put(self, request): + serializer = TwoFactorAuthCodeSerializer(data=request.data) + if serializer.is_valid(): + user = request.user + code = serializer.data["code"] + if OtpAuth(user.tfa_token).valid_totp(code): + user.two_factor_auth = False + user.save() + else: + return error_response(u"验证码错误") + else: + return serializer_invalid_response(serializer) diff --git a/contest/models.py b/contest/models.py index 591b54a..fc863ff 100644 --- a/contest/models.py +++ b/contest/models.py @@ -110,7 +110,8 @@ class ContestRank(models.Model): # 之前已经提交过,但是是错误的,这次提交是正确的。错误的题目不计入罚时 self.total_time += (info["ac_time"] + info["error_number"] * 20 * 60) problem = ContestProblem.objects.get(id=submission.problem_id) - if problem.total_accepted_number == 0: + # 更新题目计数器在前 所以是1 + if problem.total_accepted_number == 1: info["is_first_ac"] = True else: @@ -128,7 +129,7 @@ class ContestRank(models.Model): self.total_time += info["ac_time"] problem = ContestProblem.objects.get(id=submission.problem_id) - if problem.total_accepted_number == 0: + if problem.total_accepted_number == 1: info["is_first_ac"] = True else: diff --git a/db1.sqlite3 b/db1.sqlite3 deleted file mode 100644 index e69de29..0000000 diff --git a/dockerfiles/oj_web_server/Dockerfile b/dockerfiles/oj_web_server/Dockerfile index 07a73e9..93623f7 100644 --- a/dockerfiles/oj_web_server/Dockerfile +++ b/dockerfiles/oj_web_server/Dockerfile @@ -3,6 +3,6 @@ ENV PYTHONBUFFERED 1 RUN mkdir -p /code/log /code/test_case /code/upload WORKDIR /code ADD requirements.txt /code/ -RUN pip install -r requirements.txt +RUN pip install -i http://pypi.douban.com/simple -r requirements.txt --trusted-host pypi.douban.com EXPOSE 8010 -CMD supervisord \ No newline at end of file +CMD supervisord diff --git a/dockerfiles/oj_web_server/requirements.txt b/dockerfiles/oj_web_server/requirements.txt index 56676a9..32260e9 100644 --- a/dockerfiles/oj_web_server/requirements.txt +++ b/dockerfiles/oj_web_server/requirements.txt @@ -11,4 +11,6 @@ supervisor pillow jsonfield Envelopes -huey \ No newline at end of file +celery +django-celery +qrcode diff --git a/dockerfiles/oj_web_server/task_queue.conf b/dockerfiles/oj_web_server/task_queue.conf index 39f837e..5a9b13f 100644 --- a/dockerfiles/oj_web_server/task_queue.conf +++ b/dockerfiles/oj_web_server/task_queue.conf @@ -1,6 +1,6 @@ -[program:mq] +[program:task_queue] -command=python manage.py run_huey +command=python manage.py celeryd -B -l DEBUG directory=/code/ user=root diff --git a/judge_dispatcher/migrations/0003_auto_20151223_0029.py b/judge_dispatcher/migrations/0003_auto_20151223_0029.py new file mode 100644 index 0000000..fe2e13a --- /dev/null +++ b/judge_dispatcher/migrations/0003_auto_20151223_0029.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge_dispatcher', '0002_auto_20151207_2310'), + ] + + operations = [ + migrations.AddField( + model_name='judgeserver', + name='create_time', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='judgeserver', + name='name', + field=models.CharField(default='judger', max_length=30), + preserve_default=False, + ), + ] diff --git a/judge_dispatcher/models.py b/judge_dispatcher/models.py index 7ec5af8..3bac947 100644 --- a/judge_dispatcher/models.py +++ b/judge_dispatcher/models.py @@ -3,6 +3,7 @@ from django.db import models class JudgeServer(models.Model): + name = models.CharField(max_length=30) ip = models.GenericIPAddressField() port = models.IntegerField() # 这个服务器最大可能运行的判题实例数量 @@ -14,6 +15,7 @@ class JudgeServer(models.Model): lock = models.BooleanField(default=False) # status 为 false 的时候代表不使用这个服务器 status = models.BooleanField(default=True) + create_time = models.DateTimeField(auto_now_add=True, blank=True, null=True) def use_judge_instance(self): # 因为use 和 release 中间是判题时间,可能这个 model 的数据已经被修改了,所以不能直接使用self.xxx,否则取到的是旧数据 diff --git a/judge_dispatcher/serializers.py b/judge_dispatcher/serializers.py new file mode 100644 index 0000000..4328eee --- /dev/null +++ b/judge_dispatcher/serializers.py @@ -0,0 +1,29 @@ +# coding=utf-8 +import json +from rest_framework import serializers +from .models import JudgeServer + + +class CreateJudgesSerializer(serializers.Serializer): + name = serializers.CharField(max_length=30) + ip = serializers.IPAddressField() + port = serializers.IntegerField() + # 这个服务器最大可能运行的判题实例数量 + max_instance_number = serializers.IntegerField() + token = serializers.CharField(max_length=30) + + +class EditJudgesSerializer(serializers.Serializer): + id = serializers.IntegerField() + name = serializers.CharField(max_length=30) + ip = serializers.IPAddressField() + port = serializers.IntegerField() + # 这个服务器最大可能运行的判题实例数量 + max_instance_number = serializers.IntegerField() + token = serializers.CharField(max_length=30) + status = serializers.BooleanField() + + +class JudgesSerializer(serializers.ModelSerializer): + class Meta: + model = JudgeServer diff --git a/judge_dispatcher/tasks.py b/judge_dispatcher/tasks.py index 244c907..51811b1 100644 --- a/judge_dispatcher/tasks.py +++ b/judge_dispatcher/tasks.py @@ -32,7 +32,7 @@ class JudgeDispatcher(object): if servers.exists(): return servers.first() - def judge(self, is_waiting_task=False): + def judge(self): self.submission.judge_start_time = int(time.time() * 1000) with transaction.atomic(): @@ -89,9 +89,9 @@ class JudgeDispatcher(object): submission = Submission.objects.get(id=waiting_submission.submission_id) waiting_submission.delete() - _judge(submission, time_limit=waiting_submission.time_limit, - memory_limit=waiting_submission.memory_limit, test_case_id=waiting_submission.test_case_id, - is_waiting_task=True) + _judge.delay(submission, time_limit=waiting_submission.time_limit, + memory_limit=waiting_submission.memory_limit, test_case_id=waiting_submission.test_case_id, + is_waiting_task=True) def update_problem_status(self): problem = Problem.objects.get(id=self.submission.problem_id) @@ -113,13 +113,14 @@ class JudgeDispatcher(object): # 普通题目的话,到这里就结束了 def update_contest_problem_status(self): - # 能运行到这里的都是比赛题目 + # 能运行到这里的都是比赛题目a contest = Contest.objects.get(id=self.submission.contest_id) if contest.status != CONTEST_UNDERWAY: logger.info("Contest debug mode, id: " + str(contest.id) + ", submission id: " + self.submission.id) return with transaction.atomic(): - contest_problem = ContestProblem.objects.select_for_update().get(contest=contest, id=self.submission.problem_id) + contest_problem = ContestProblem.objects.select_for_update().get(contest=contest, + id=self.submission.problem_id) contest_problem.add_submission_number() diff --git a/judge_dispatcher/views.py b/judge_dispatcher/views.py new file mode 100644 index 0000000..75f0999 --- /dev/null +++ b/judge_dispatcher/views.py @@ -0,0 +1,71 @@ +# coding=utf-8 +from rest_framework.views import APIView + +from account.decorators import super_admin_required +from utils.shortcuts import success_response, serializer_invalid_response, error_response, paginate +from .serializers import CreateJudgesSerializer, JudgesSerializer, EditJudgesSerializer +from .models import JudgeServer + + +class AdminJudgeServerAPIView(APIView): + @super_admin_required + def post(self, request): + """ + 添加判题服务器 json api接口 + --- + request_serializer: CreateJudgesSerializer + response_serializer: JudgesSerializer + """ + serializer = CreateJudgesSerializer(data=request.data) + if serializer.is_valid(): + data = serializer.data + judge_server = JudgeServer.objects.create(name=data["name"], ip=data["ip"], port=data["port"], + max_instance_number=data["max_instance_number"], + token=data["token"], + created_by=request.user) + return success_response(JudgesSerializer(judge_server).data) + else: + return serializer_invalid_response(serializer) + + @super_admin_required + def put(self, request): + """ + 修改判题服务器信息 json api接口 + --- + request_serializer: EditJudgesSerializer + response_serializer: JudgesSerializer + """ + serializer = EditJudgesSerializer(data=request.data) + if serializer.is_valid(): + data = serializer.data + try: + judge_server = JudgeServer.objects.get(pk=data["id"]) + except JudgeServer.DoesNotExist: + return error_response(u"此判题服务器不存在!") + + judge_server.name = data["name"] + judge_server.ip = data["ip"] + judge_server.port = data["port"] + judge_server.max_instance_number = data["max_instance_number"] + judge_server.token = data["token"] + judge_server.status = data["status"] + judge_server.save() + return success_response(JudgesSerializer(judge_server).data) + else: + return serializer_invalid_response(serializer) + + @super_admin_required + def get(self, request): + """ + 获取全部判题服务器 + """ + judge_server_id = request.GET.get("judge_server_id", None) + if judge_server_id: + try: + judge_server = JudgeServer.objects.get(id=judge_server_id) + except JudgeServer.DoesNotExist: + return error_response(u"判题服务器不存在") + return success_response(JudgesSerializer(judge_server).data) + judge_server = JudgeServer.objects.all() + + return paginate(request, judge_server, JudgesSerializer) diff --git a/monitor/__init__.py b/monitor/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/monitor/views.py b/monitor/views.py deleted file mode 100644 index f502579..0000000 --- a/monitor/views.py +++ /dev/null @@ -1,19 +0,0 @@ -# coding=utf-8 -import redis -import datetime -from rest_framework.views import APIView -from judge.result import result -from django.conf import settings -from utils.shortcuts import success_response -from submission.models import Submission - - -class QueueLengthMonitorAPIView(APIView): - def get(self, request): - r = redis.Redis(host=settings.redis_config["host"], port=settings.redis_config["port"], db=settings.redis_config["db"]) - waiting_number = r.get("judge_queue_length") - if waiting_number is None: - waiting_number = 0 - now = datetime.datetime.now() - return success_response({"time": ":".join([str(now.hour), str(now.minute), str(now.second)]), - "count": waiting_number}) \ No newline at end of file diff --git a/oj/__init__.py b/oj/__init__.py index f420637..44aca8d 100644 --- a/oj/__init__.py +++ b/oj/__init__.py @@ -7,3 +7,8 @@ |___/ |___/ |_| https://github.com/QingdaoU/OnlineJudge """ +from __future__ import absolute_import + +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app \ No newline at end of file diff --git a/oj/celery.py b/oj/celery.py new file mode 100644 index 0000000..f20f9dc --- /dev/null +++ b/oj/celery.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import + +import os + +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oj.settings') + +from django.conf import settings + +app = Celery('oj') + +# Using a string here means the worker will not have to +# pickle the object when using Windows. +app.config_from_object('django.conf:settings') + +# load task modules from all registered Django app configs. +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) \ No newline at end of file diff --git a/oj/local_settings.py b/oj/local_settings.py index 794a7b6..8fa3b8a 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -28,6 +28,12 @@ REDIS_QUEUE = { "db": 2 } + +# for celery +BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"])) +ACCEPT_CONTENT = ['json'] + + DEBUG = True ALLOWED_HOSTS = [] diff --git a/oj/server_settings.py b/oj/server_settings.py index afec6d1..98eae98 100644 --- a/oj/server_settings.py +++ b/oj/server_settings.py @@ -37,6 +37,12 @@ REDIS_QUEUE = { "db": 2 } + +# for celery +BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"])) +ACCEPT_CONTENT = ['json'] + + DEBUG = False ALLOWED_HOSTS = ['*'] diff --git a/oj/settings.py b/oj/settings.py index f585703..96c2795 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -10,7 +10,7 @@ https://docs.djangoproject.com/en/1.8/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.8/ref/settings/ """ - +from __future__ import absolute_import # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os @@ -22,6 +22,9 @@ if ENV == "local": elif ENV == "server": from .server_settings import * +import djcelery +djcelery.setup_loader() + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -53,7 +56,8 @@ INSTALLED_APPS = ( 'judge_dispatcher', 'rest_framework', - 'huey.djhuey', + 'djcelery', + ) if DEBUG: @@ -186,14 +190,6 @@ WEBSITE_INFO = {"website_name": "qduoj", "website_footer": u"青岛大学信息工程学院 创新实验室 京ICP备15062075号-1", "url": "https://qduoj.com"} -HUEY = { - 'backend': 'huey.backends.redis_backend', - 'name': 'task_queue', - 'connection': {'host': REDIS_QUEUE["host"], 'port': REDIS_QUEUE["port"], 'db': REDIS_QUEUE["db"]}, - 'always_eager': False, # Defaults to False when running via manage.py run_huey - # Options to pass into the consumer when running ``manage.py run_huey`` - 'consumer_options': {'workers': 50}, -} SMTP_CONFIG = {"smtp_server": "smtp.mxhichina.com", "email": "noreply@qduoj.com", diff --git a/oj/urls.py b/oj/urls.py index 77a92b6..fea7441 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -6,7 +6,8 @@ from django.views.generic import TemplateView from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterAPIView, UserChangePasswordAPIView, EmailCheckAPIView, UserAdminAPIView, UserInfoAPIView, ResetPasswordAPIView, - ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView) + ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView, + TwoFactorAuthAPIView) from announcement.views import AnnouncementAdminAPIView @@ -22,11 +23,9 @@ from admin.views import AdminTemplateView from problem.views import TestCaseUploadAPIView, ProblemTagAdminAPIView, ProblemAdminAPIView from submission.views import (SubmissionAPIView, SubmissionAdminAPIView, ContestSubmissionAPIView, SubmissionShareAPIView, SubmissionRejudgeAdminAPIView) -from monitor.views import QueueLengthMonitorAPIView +from judge_dispatcher.views import AdminJudgeServerAPIView from utils.views import SimditorImageUploadAPIView - - urlpatterns = [ url("^$", "account.views.index_page", name="index_page"), @@ -55,7 +54,6 @@ urlpatterns = [ url(r'^api/contest/submission/$', ContestSubmissionAPIView.as_view(), name="contest_submission_api"), url(r'^api/submission/$', SubmissionAPIView.as_view(), name="submission_api"), url(r'^api/group_join/$', JoinGroupAPIView.as_view(), name="group_join_api"), - url(r'^api/admin/upload_image/$', SimditorImageUploadAPIView.as_view(), name="simditor_upload_image"), url(r'^api/admin/announcement/$', AnnouncementAdminAPIView.as_view(), name="announcement_admin_api"), @@ -64,17 +62,18 @@ urlpatterns = [ url(r'^api/admin/group/$', GroupAdminAPIView.as_view(), name="group_admin_api"), url(r'^api/admin/group_member/$', GroupMemberAdminAPIView.as_view(), name="group_member_admin_api"), url(r'^api/admin/group/promot_as_admin/$', GroupPrometAdminAPIView.as_view(), name="group_promote_admin_api"), - url(r'^api/admin/problem/$', ProblemAdminAPIView.as_view(), name="problem_admin_api"), url(r'^api/admin/contest_problem/$', ContestProblemAdminAPIView.as_view(), name="contest_problem_admin_api"), - url(r'^api/admin/contest_problem/public/', MakeContestProblemPublicAPIView.as_view(), name="make_contest_problem_public"), + url(r'^api/admin/contest_problem/public/', MakeContestProblemPublicAPIView.as_view(), + name="make_contest_problem_public"), url(r'^api/admin/test_case_upload/$', TestCaseUploadAPIView.as_view(), name="test_case_upload_api"), url(r'^api/admin/tag/$', ProblemTagAdminAPIView.as_view(), name="problem_tag_admin_api"), url(r'^api/admin/join_group_request/$', JoinGroupRequestAdminAPIView.as_view(), name="join_group_request_admin_api"), url(r'^api/admin/submission/$', SubmissionAdminAPIView.as_view(), name="submission_admin_api_view"), - url(r'^api/admin/monitor/$', QueueLengthMonitorAPIView.as_view(), name="queue_length_monitor_api"), + + url(r'^api/admin/judges/$', AdminJudgeServerAPIView.as_view(), name="judges_admin_api"), url(r'^contest/(?P\d+)/problem/(?P\d+)/$', "contest.views.contest_problem_page", name="contest_problem_page"), @@ -93,14 +92,12 @@ urlpatterns = [ url(r'^contests/$', "contest.views.contest_list_page", name="contest_list_page"), url(r'^contests/(?P\d+)/$', "contest.views.contest_list_page", name="contest_list_page"), - url(r'^problem/(?P\d+)/$', "problem.views.problem_page", name="problem_page"), url(r'^problems/$', "problem.views.problem_list_page", name="problem_list_page"), url(r'^problems/(?P\d+)/$', "problem.views.problem_list_page", name="problem_list_page"), url(r'^problem/(?P\d+)/submissions/$', "submission.views.problem_my_submissions_list_page", name="problem_my_submissions_page"), - url(r'^submission/(?P\w+)/$', "submission.views.my_submission", name="my_submission_page"), url(r'^submissions/$', "submission.views.my_submission_list_page", name="my_submission_list_page"), url(r'^submissions/(?P\d+)/$', "submission.views.my_submission_list_page", name="my_submission_list_page"), @@ -127,14 +124,18 @@ urlpatterns = [ url(r'^api/apply_reset_password/$', ApplyResetPasswordAPIView.as_view(), name="apply_reset_password_api"), url(r'^api/reset_password/$', ResetPasswordAPIView.as_view(), name="apply_reset_password_api"), - url(r'^account/settings/$', TemplateView.as_view(template_name="oj/account/settings.html"), name="account_setting_page"), - url(r'^account/settings/avatar/$', TemplateView.as_view(template_name="oj/account/avatar.html"), name="avatar_settings_page"), + url(r'^account/settings/$', TemplateView.as_view(template_name="oj/account/settings.html"), + name="account_setting_page"), + url(r'^account/settings/avatar/$', TemplateView.as_view(template_name="oj/account/avatar.html"), + name="avatar_settings_page"), url(r'^account/sso/$', SSOAPIView.as_view(), name="sso_api"), url(r'^api/account/userprofile/$', UserProfileAPIView.as_view(), name="userprofile_api"), url(r'^reset_password/$', TemplateView.as_view(template_name="oj/account/apply_reset_password.html"), name="apply_reset_password_page"), - url(r'^reset_password/t/(?P\w+)/$', "account.views.reset_password_page", name="reset_password_page") + url(r'^reset_password/t/(?P\w+)/$', "account.views.reset_password_page", name="reset_password_page"), + url(r'^api/two_factor_auth/$', TwoFactorAuthAPIView.as_view(), name="two_factor_auth_api"), + url(r'^two_factor_auth/$', TemplateView.as_view(template_name="oj/account/two_factor_auth.html"), name="two_factor_auth_page"), ] if settings.DEBUG: - urlpatterns.append(url(r'^docs/', include('rest_framework_swagger.urls'))) \ No newline at end of file + urlpatterns.append(url(r'^docs/', include('rest_framework_swagger.urls'))) diff --git a/static/src/css/oj.css b/static/src/css/oj.css index 52701b3..a04f0a0 100644 --- a/static/src/css/oj.css +++ b/static/src/css/oj.css @@ -118,3 +118,13 @@ li.problem-tag { padding-top: 7.5px; padding-bottom: 7.5px; } + + +#tfa-qrcode{ + height: 40%; + width: 40%; +} + +#tfa-area{ + display: none; +} \ No newline at end of file diff --git a/static/src/js/app/admin/admin.js b/static/src/js/app/admin/admin.js index 2c527de..ec75de1 100644 --- a/static/src/js/app/admin/admin.js +++ b/static/src/js/app/admin/admin.js @@ -22,46 +22,48 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($, } var superAdminNav = [ - { name: "首页", - children: [{name: "主页", hash: "#index/index"}, - {name: "监控", hash: "#monitor/monitor"}] - }, - { - name: "通用", - children: [{name: "公告管理", hash: "#announcement/announcement"}, - {name: "用户管理", hash: "#user/user_list"}] - }, - { - name: "题目管理", - children: [{name: "题目列表", hash: "#problem/problem_list"}, - {name: "创建题目", hash: "#problem/add_problem"}] - }, - { - name: "比赛管理", - children: [{name: "比赛列表", hash: "#contest/contest_list"}, - {name: "创建比赛", hash: "#contest/add_contest"}] - }, - { - name: "小组管理", - children: [{name: "小组列表", hash: "#group/group"}, - {name: "加入小组请求", hash: "#group/join_group_request_list"}] - } + { + name: "首页", + children: [{name: "主页", hash: "#index/index"}, + {name: "判题服务器", hash: "#judges/judges"}] + }, + { + name: "通用", + children: [{name: "公告管理", hash: "#announcement/announcement"}, + {name: "用户管理", hash: "#user/user_list"}] + }, + { + name: "题目管理", + children: [{name: "题目列表", hash: "#problem/problem_list"}, + {name: "创建题目", hash: "#problem/add_problem"}] + }, + { + name: "比赛管理", + children: [{name: "比赛列表", hash: "#contest/contest_list"}, + {name: "创建比赛", hash: "#contest/add_contest"}] + }, + { + name: "小组管理", + children: [{name: "小组列表", hash: "#group/group"}, + {name: "加入小组请求", hash: "#group/join_group_request_list"}] + } ]; var adminNav = [ - { name: "首页", - children: [{name: "主页", hash: "#index/index"}] - }, - { - name: "比赛管理", - children: [{name: "比赛列表", hash: "#contest/contest_list"}, - {name: "创建比赛", hash: "#contest/add_contest"}] - }, - { - name: "小组管理", - children: [{name: "小组列表", hash: "#group/group"}, - {name: "加入小组请求", hash: "#group/join_group_request_list"}] - } + { + name: "首页", + children: [{name: "主页", hash: "#index/index"}] + }, + { + name: "比赛管理", + children: [{name: "比赛列表", hash: "#contest/contest_list"}, + {name: "创建比赛", hash: "#contest/add_contest"}] + }, + { + name: "小组管理", + children: [{name: "小组列表", hash: "#group/group"}, + {name: "加入小组请求", hash: "#group/join_group_request_list"}] + } ]; var vm = avalon.define({ @@ -79,7 +81,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($, hide_loading: function () { $("#loading-gif").hide(); }, - getLiId: function(hash){ + getLiId: function (hash) { return hash.replace("#", "li-").replace("/", "-"); } }); @@ -89,21 +91,20 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($, url: "/api/user/", method: "get", dataType: "json", - success: function(data){ - if(!data.code){ + success: function (data) { + if (!data.code) { vm.username = data.data.username; vm.adminType = data.data.admin_type; - if (data.data.admin_type == 2){ + if (data.data.admin_type == 2) { vm.adminNavList = superAdminNav; } - else{ + else { vm.adminNavList = adminNav; } } } }); - avalon.scan(); @@ -115,12 +116,14 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($, show_template("template/" + hash + ".html"); } }; - setTimeout(function(){li_active("#li-" + hash.replace("/", "-"));}, 500); + setTimeout(function () { + li_active("#li-" + hash.replace("/", "-")); + }, 500); $.ajaxSetup({ - beforeSend: csrfTokenHeader, + beforeSend: csrfTokenHeader, dataType: "json", - error: function(){ + error: function () { bsAlert("请求失败"); } }); diff --git a/static/src/js/app/admin/judges/judges.js b/static/src/js/app/admin/judges/judges.js new file mode 100644 index 0000000..6e651f2 --- /dev/null +++ b/static/src/js/app/admin/judges/judges.js @@ -0,0 +1,143 @@ +require(["jquery", "avalon", "csrfToken", "bsAlert", "validator", "pager"], + function ($, avalon, csrfTokenHeader, bsAlert, editor) { + avalon.ready(function () { + + if (avalon.vmodels.judges) { + var vm = avalon.vmodels.judges; + } + else { + var vm = avalon.define({ + $id: "judges", + judgesList: [], + isEditing: false, + showEnableOnly: false, + + //编辑器同步变量 + max_instance_number: 0, + ipAddress: "", + port: 0, + status: true, + judgesId: -1, + name: "", + token: "", + id: 0, + pager: { + getPage: function (page) { + getPage(page); + } + }, + editJudges: function (judges) { + vm.id = judges.id; + vm.name = judges.name; + vm.judgesId = judges.id; + vm.status = judges.status; + vm.port = judges.port; + vm.ipAddress = judges.ip; + vm.max_instance_number = judges.max_instance_number; + vm.token = judges.token; + vm.isEditing = true; + }, + cancelEdit: function () { + vm.isEditing = false; + } + }); + vm.$watch("showEnableOnly", function () { + getPage(1); + avalon.vmodels.judgesPager.currentPage = 1; + }); + } + + function getPage(page) { + var url = "/api/admin/judges/?paging=true&page=" + page + "&page_size=20"; + if (vm.showEnableNnly) + url += "&status=true"; + $.ajax({ + url: url, + method: "get", + success: function (data) { + if (!data.code) { + vm.judgesList = data.data.results; + avalon.vmodels.judgesPager.totalPage = data.data.total_page; + } + else { + bsAlert(data.data); + } + } + }); + } + + $("#judges-form").validator().on('submit', function (e) { + if (!e.isDefaultPrevented()) { + var name = $("#name").val(); + var max_instance_number = $("#max_instance_number").val(); + var ip = $("#ipAddress").val(); + var port = $("#port").val(); + var token = $("#token").val(); + $.ajax({ + url: "/api/admin/judges/", + contentType: "application/json", + data: JSON.stringify({ + name: name, + ip: ip, + port: port, + token: token, + max_instance_number: max_instance_number + }), + dataType: "json", + method: "post", + success: function (data) { + if (!data.code) { + bsAlert("提交成功!"); + $("#name").val(""); + $("#max_instance_number").val(""); + $("#ipAddress").val(""); + $("#port").val(""); + $("#token").val(""); + getPage(1); + } else { + bsAlert(data.data); + } + } + }); + return false; + } + }); + + $("#edit-judges-form").validator().on('submit', function (e) { + if (!e.isDefaultPrevented()) { + var name = vm.name; + var max_instance_number = vm.max_instance_number; + var ip = vm.ipAddress; + var port = vm.port; + var token = vm.token; + var status = vm.status; + var id = vm.id; + $.ajax({ + url: "/api/admin/judges/", + contentType: "application/json", + data: JSON.stringify({ + id: id, + name: name, + ip: ip, + port: port, + token: token, + max_instance_number: max_instance_number, + status: status + }), + dataType: "json", + method: "put", + success: function (data) { + if (!data.code) { + bsAlert("提交成功!"); + getPage(1); + } else { + bsAlert(data.data); + } + } + }); + return false; + } + }); + }); + avalon.scan(); + }); \ No newline at end of file diff --git a/static/src/js/app/admin/monitor/monitor.js b/static/src/js/app/admin/monitor/monitor.js deleted file mode 100644 index 1a0e46f..0000000 --- a/static/src/js/app/admin/monitor/monitor.js +++ /dev/null @@ -1,48 +0,0 @@ -require(["jquery", "chart"], function ($, Chart) { - var data = { - labels: ["初始化"], - datasets: [ - { - label: "队列长度", - fillColor: "rgba(255,255,255,0.2)", - strokeColor: "rgba(151,187,205,1)", - pointColor: "rgba(151,187,205,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(151,187,205,1)", - data: [0] - } - ] - }; - var chart = new Chart($("#waiting-queue-chart").get(0).getContext("2d")).Line(data); - - var dataCounter = 0; - - function getMonitorData(){ - var hash = location.hash; - if (hash != "#monitor/monitor"){ - clearInterval(intervalId); - } - $.ajax({ - url: "/api/admin/monitor/", - method: "get", - dataType: "json", - success: function(data){ - if(!data.code){ - chart.addData([data.data["count"]], data.data["time"]) - dataCounter ++; - } - } - }) - } - - $("#clear-chart-data").click(function(){ - for(var i = 0;i < dataCounter;i++) { - chart.removeData(); - } - dataCounter = 0; - }); - - var intervalId = setInterval(getMonitorData, 3000); - -}); \ No newline at end of file diff --git a/static/src/js/app/oj/account/login.js b/static/src/js/app/oj/account/login.js index 8e1351d..3e35fd0 100644 --- a/static/src/js/app/oj/account/login.js +++ b/static/src/js/app/oj/account/login.js @@ -4,23 +4,31 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c if (!e.isDefaultPrevented()) { var username = $("#username").val(); var password = $("#password").val(); - var captcha = $("#captcha").val(); + var tfaCode = $("#tfa-code").val(); + console.log(tfaCode); + if(tfaCode.length && tfaCode.length != 6){ + bsAlert("验证码为六位数字"); + return false; + } $.ajax({ beforeSend: csrfTokenHeader, url: "/api/login/", - data: {username: username, password: password, captcha: captcha}, + data: {username: username, password: password, tfa_code: tfaCode}, dataType: "json", method: "post", success: function (data) { if (!data.code) { + if(data.data == "tfa_required"){ + $("#tfa-area").show(); + return false; + } function getLocationVal(id){ var temp = unescape(location.search).split(id+"=")[1] || ""; return temp.indexOf("&")>=0 ? temp.split("&")[0] : temp; } var from = getLocationVal("__from"); if(from != ""){ - console.log(from); window.location.href = from; } else{ @@ -28,7 +36,6 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c } } else { - refresh_captcha(); bsAlert(data.data); } }, @@ -40,11 +47,5 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c return false; } }); - function refresh_captcha(){ - $("#captcha-img")[0].src = "/captcha/?" + Math.random(); - $("#captcha")[0].value = ""; - } - $("#captcha-img").click(function(){ - refresh_captcha(); - }); + }); \ No newline at end of file diff --git a/static/src/js/app/oj/account/twoFactorAuth.js b/static/src/js/app/oj/account/twoFactorAuth.js new file mode 100644 index 0000000..7c89e5b --- /dev/null +++ b/static/src/js/app/oj/account/twoFactorAuth.js @@ -0,0 +1,27 @@ +require(["jquery", "bsAlert", "csrfToken"], function ($, bsAlert, csrfTokenHeader) { + $("#tfa_submit").click(function(){ + var code = $("#tfa_code").val(); + if (code.length != 6){ + bsAlert("验证码是6位数字"); + return; + } + $.ajax({ + beforeSend: csrfTokenHeader, + url: "/api/two_factor_auth/", + data: {code: code}, + dataType: "json", + method: "post", + success: function(data){ + if(data.code){ + bsAlert(data.data); + } + else{ + bsAlert("两步验证开启成功"); + location.reload(); + } + } + }) + + }) +}); + diff --git a/static/src/js/build.js b/static/src/js/build.js index 8c8de62..df5e234 100644 --- a/static/src/js/build.js +++ b/static/src/js/build.js @@ -54,31 +54,31 @@ //以下都是页面 script 标签引用的js announcement_0_pack: "app/admin/announcement/announcement", userList_1_pack: "app/admin/user/userList", - problem_2_pack: "app/oj/problem/problem", - submissionList_3_pack: "app/admin/problem/submissionList", - contestCountdown_4_pack: "app/oj/contest/contestCountdown", - avatar_5_pack: "app/oj/account/avatar", - addProblem_6_pack: "app/admin/problem/addProblem", - problem_7_pack: "app/admin/problem/problem", - contestList_8_pack: "app/admin/contest/contestList", - admin_9_pack: "app/admin/admin", - login_10_pack: "app/oj/account/login", - applyResetPassword_11_pack: "app/oj/account/applyResetPassword", - addContest_12_pack: "app/admin/contest/addContest", - contestPassword_13_pack: "app/oj/contest/contestPassword", - changePassword_14_pack: "app/oj/account/changePassword", - monitor_15_pack: "app/admin/monitor/monitor", - editProblem_16_pack: "app/admin/contest/editProblem", - joinGroupRequestList_17_pack: "app/admin/group/joinGroupRequestList", - group_18_pack: "app/oj/group/group", - contestProblemList_19_pack: "app/admin/contest/contestProblemList", - editProblem_20_pack: "app/admin/problem/editProblem", - register_21_pack: "app/oj/account/register", - groupDetail_22_pack: "app/admin/group/groupDetail", - editContest_23_pack: "app/admin/contest/editContest", - resetPassword_24_pack: "app/oj/account/resetPassword", - group_25_pack: "app/admin/group/group", - settings_26_pack: "app/oj/account/settings" + twoFactorAuth_2_pack: "app/oj/account/twoFactorAuth", + problem_3_pack: "app/oj/problem/problem", + submissionList_4_pack: "app/admin/problem/submissionList", + contestCountdown_5_pack: "app/oj/contest/contestCountdown", + avatar_6_pack: "app/oj/account/avatar", + addProblem_7_pack: "app/admin/problem/addProblem", + problem_8_pack: "app/admin/problem/problem", + contestList_9_pack: "app/admin/contest/contestList", + admin_10_pack: "app/admin/admin", + login_11_pack: "app/oj/account/login", + applyResetPassword_12_pack: "app/oj/account/applyResetPassword", + addContest_13_pack: "app/admin/contest/addContest", + contestPassword_14_pack: "app/oj/contest/contestPassword", + changePassword_15_pack: "app/oj/account/changePassword", + editProblem_17_pack: "app/admin/contest/editProblem", + joinGroupRequestList_18_pack: "app/admin/group/joinGroupRequestList", + group_19_pack: "app/oj/group/group", + contestProblemList_20_pack: "app/admin/contest/contestProblemList", + editProblem_21_pack: "app/admin/problem/editProblem", + register_22_pack: "app/oj/account/register", + groupDetail_23_pack: "app/admin/group/groupDetail", + editContest_24_pack: "app/admin/contest/editContest", + resetPassword_25_pack: "app/oj/account/resetPassword", + group_26_pack: "app/admin/group/group", + settings_27_pack: "app/oj/account/settings" }, shim: { avalon: { @@ -96,79 +96,79 @@ name: "userList_1_pack" }, { - name: "problem_2_pack" + name: "twoFactorAuth_2_pack" }, { - name: "submissionList_3_pack" + name: "problem_3_pack" }, { - name: "contestCountdown_4_pack" + name: "submissionList_4_pack" }, { - name: "avatar_5_pack" + name: "contestCountdown_5_pack" }, { - name: "addProblem_6_pack" + name: "avatar_6_pack" }, { - name: "problem_7_pack" + name: "addProblem_7_pack" }, { - name: "contestList_8_pack" + name: "problem_8_pack" }, { - name: "admin_9_pack" + name: "contestList_9_pack" }, { - name: "login_10_pack" + name: "admin_10_pack" }, { - name: "applyResetPassword_11_pack" + name: "login_11_pack" }, { - name: "addContest_12_pack" + name: "applyResetPassword_12_pack" }, { - name: "contestPassword_13_pack" + name: "addContest_13_pack" }, { - name: "changePassword_14_pack" + name: "contestPassword_14_pack" }, { - name: "monitor_15_pack" + name: "changePassword_15_pack" }, { - name: "editProblem_16_pack" + name: "editProblem_17_pack" }, { - name: "joinGroupRequestList_17_pack" + name: "joinGroupRequestList_18_pack" }, { - name: "group_18_pack" + name: "group_19_pack" }, { - name: "contestProblemList_19_pack" + name: "contestProblemList_20_pack" }, { - name: "editProblem_20_pack" + name: "editProblem_21_pack" }, { - name: "register_21_pack" + name: "register_22_pack" }, { - name: "groupDetail_22_pack" + name: "groupDetail_23_pack" }, { - name: "editContest_23_pack" + name: "editContest_24_pack" }, { - name: "resetPassword_24_pack" + name: "resetPassword_25_pack" }, { - name: "group_25_pack" + name: "group_26_pack" }, { - name: "settings_26_pack" + name: "settings_27_pack" } ], optimizeCss: "standard", diff --git a/static/src/js/config.js b/static/src/js/config.js index ee6f52b..531d349 100644 --- a/static/src/js/config.js +++ b/static/src/js/config.js @@ -56,31 +56,31 @@ var require = { //以下都是页面 script 标签引用的js announcement_0_pack: "app/admin/announcement/announcement", userList_1_pack: "app/admin/user/userList", - problem_2_pack: "app/oj/problem/problem", - submissionList_3_pack: "app/admin/problem/submissionList", - contestCountdown_4_pack: "app/oj/contest/contestCountdown", - avatar_5_pack: "app/oj/account/avatar", - addProblem_6_pack: "app/admin/problem/addProblem", - problem_7_pack: "app/admin/problem/problem", - contestList_8_pack: "app/admin/contest/contestList", - admin_9_pack: "app/admin/admin", - login_10_pack: "app/oj/account/login", - applyResetPassword_11_pack: "app/oj/account/applyResetPassword", - addContest_12_pack: "app/admin/contest/addContest", - contestPassword_13_pack: "app/oj/contest/contestPassword", - changePassword_14_pack: "app/oj/account/changePassword", - monitor_15_pack: "app/admin/monitor/monitor", - editProblem_16_pack: "app/admin/contest/editProblem", - joinGroupRequestList_17_pack: "app/admin/group/joinGroupRequestList", - group_18_pack: "app/oj/group/group", - contestProblemList_19_pack: "app/admin/contest/contestProblemList", - editProblem_20_pack: "app/admin/problem/editProblem", - register_21_pack: "app/oj/account/register", - groupDetail_22_pack: "app/admin/group/groupDetail", - editContest_23_pack: "app/admin/contest/editContest", - resetPassword_24_pack: "app/oj/account/resetPassword", - group_25_pack: "app/admin/group/group", - settings_26_pack: "app/oj/account/settings", + twoFactorAuth_2_pack: "app/oj/account/twoFactorAuth", + problem_3_pack: "app/oj/problem/problem", + submissionList_4_pack: "app/admin/problem/submissionList", + contestCountdown_5_pack: "app/oj/contest/contestCountdown", + avatar_6_pack: "app/oj/account/avatar", + addProblem_7_pack: "app/admin/problem/addProblem", + problem_8_pack: "app/admin/problem/problem", + contestList_9_pack: "app/admin/contest/contestList", + admin_10_pack: "app/admin/admin", + login_11_pack: "app/oj/account/login", + applyResetPassword_12_pack: "app/oj/account/applyResetPassword", + addContest_13_pack: "app/admin/contest/addContest", + contestPassword_14_pack: "app/oj/contest/contestPassword", + changePassword_15_pack: "app/oj/account/changePassword", + editProblem_17_pack: "app/admin/contest/editProblem", + joinGroupRequestList_18_pack: "app/admin/group/joinGroupRequestList", + group_19_pack: "app/oj/group/group", + contestProblemList_20_pack: "app/admin/contest/contestProblemList", + editProblem_21_pack: "app/admin/problem/editProblem", + register_22_pack: "app/oj/account/register", + groupDetail_23_pack: "app/admin/group/groupDetail", + editContest_24_pack: "app/admin/contest/editContest", + resetPassword_25_pack: "app/oj/account/resetPassword", + group_26_pack: "app/admin/group/group", + settings_27_pack: "app/oj/account/settings" }, shim: { avalon: { diff --git a/submission/tasks.py b/submission/tasks.py index 25476e3..4267841 100644 --- a/submission/tasks.py +++ b/submission/tasks.py @@ -1,9 +1,9 @@ # coding=utf-8 -from huey.djhuey import task - +from __future__ import absolute_import +from celery import shared_task from judge_dispatcher.tasks import JudgeDispatcher -@task() -def _judge(submission, time_limit, memory_limit, test_case_id, is_waiting_task=False): - JudgeDispatcher(submission, time_limit, memory_limit, test_case_id).judge(is_waiting_task) \ No newline at end of file +@shared_task +def _judge(submission, time_limit, memory_limit, test_case_id): + JudgeDispatcher(submission, time_limit, memory_limit, test_case_id).judge() \ No newline at end of file diff --git a/submission/views.py b/submission/views.py index ccda8ac..41f5e3b 100644 --- a/submission/views.py +++ b/submission/views.py @@ -43,7 +43,7 @@ class SubmissionAPIView(APIView): problem_id=problem.id) try: - _judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id) + _judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id) except Exception as e: logger.error(e) return error_response(u"提交判题任务失败") @@ -88,7 +88,7 @@ class ContestSubmissionAPIView(APIView): code=data["code"], problem_id=problem.id) try: - _judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id) + _judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id) except Exception as e: logger.error(e) return error_response(u"提交判题任务失败") @@ -273,7 +273,7 @@ class SubmissionRejudgeAdminAPIView(APIView): except Problem.DoesNotExist: return error_response(u"题目不存在") try: - _judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id) + _judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id) except Exception as e: logger.error(e) return error_response(u"提交判题任务失败") diff --git a/template/src/admin/judges/judges.html b/template/src/admin/judges/judges.html new file mode 100644 index 0000000..11ef8e9 --- /dev/null +++ b/template/src/admin/judges/judges.html @@ -0,0 +1,131 @@ +
+

判题服务器管理

+ + + + + + + + + + + + + + + + + + + +
编号名字最大实例数量负载创建时间状态
{{ el.id }}{{ el.name }}{{ el.max_instance_number }}{{ el.workload }}{{ el.create_time|date("yyyy-MM-dd HH:mm:ss")}} + +
+
+ +
+
+ +
+ +
+

编辑判题服务器

+ +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ +
+ +
+ +    + 取消 +
+
+
+
+ +

添加判题服务器

+ +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ +
+
+ +
+ \ No newline at end of file diff --git a/template/src/oj/account/avatar.html b/template/src/oj/account/avatar.html index d551c4a..25a6d17 100644 --- a/template/src/oj/account/avatar.html +++ b/template/src/oj/account/avatar.html @@ -10,6 +10,7 @@
  • 通用设置
  • 个人信息
  • 更换头像
  • +
  • 两步验证
  • 修改密码
  • diff --git a/template/src/oj/account/change_password.html b/template/src/oj/account/change_password.html index 9fd1334..25faf1d 100644 --- a/template/src/oj/account/change_password.html +++ b/template/src/oj/account/change_password.html @@ -10,6 +10,7 @@
  • 通用设置
  • 个人信息
  • 更换头像
  • +
  • 两步验证
  • 修改密码
  • diff --git a/template/src/oj/account/login.html b/template/src/oj/account/login.html index eadb5cc..04302f1 100644 --- a/template/src/oj/account/login.html +++ b/template/src/oj/account/login.html @@ -11,22 +11,21 @@
    + data-error="请填写用户名" placeholder="用户名" autofocus required autocomplete="off">
    + data-error="请填写密码" placeholder="密码" required autocomplete="off">
    -
    -    -

    - +
    + +
    diff --git a/template/src/oj/account/settings.html b/template/src/oj/account/settings.html index 5356024..e6436bb 100644 --- a/template/src/oj/account/settings.html +++ b/template/src/oj/account/settings.html @@ -10,6 +10,7 @@
  • 通用设置
  • 个人信息
  • 更换头像
  • +
  • 两步验证
  • 修改密码
  • diff --git a/template/src/oj/account/two_factor_auth.html b/template/src/oj/account/two_factor_auth.html new file mode 100644 index 0000000..fe35d74 --- /dev/null +++ b/template/src/oj/account/two_factor_auth.html @@ -0,0 +1,41 @@ +{% extends "oj_base.html" %} +{% block title %} + 两步验证 +{% endblock %} +{% block body %} +
    + +
    + +
    + +
    + {% if not request.user.two_factor_auth %} +

    扫描二维码开启两步验证

    + + +
    +
    + + +
    + +
    + + {% else %} + + {% endif %} +
    +
    +{% endblock %} + +{% block js_block %} + +{% endblock %} diff --git a/utils/otp_auth.py b/utils/otp_auth.py new file mode 100644 index 0000000..12773c4 --- /dev/null +++ b/utils/otp_auth.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +""" + otpauth + ~~~~~~~ + + Implements two-step verification of HOTP/TOTP. + + :copyright: (c) 2013 - 2015 by Hsiaoming Yang. + :license: BSD, see LICENSE for more details. +""" +import sys +import time +import hmac +import base64 +import struct +import hashlib +import warnings + + +if sys.version_info[0] == 3: + PY2 = False + string_type = str +else: + PY2 = True + string_type = unicode + range = xrange + + +__author__ = 'Hsiaoming Yang ' +__homepage__ = 'https://github.com/lepture/otpauth' +__version__ = '1.0.1' + + +__all__ = ['OtpAuth', 'HOTP', 'TOTP', 'generate_hotp', 'generate_totp'] + + +HOTP = 'hotp' +TOTP = 'totp' + + +class OtpAuth(object): + """One Time Password Authentication. + + :param secret: A secret token for the authentication. + """ + + def __init__(self, secret): + self.secret = secret + + def hotp(self, counter=4): + """Generate a HOTP code. + + :param counter: HOTP is a counter based algorithm. + """ + return generate_hotp(self.secret, counter) + + def totp(self, period=30, timestamp=None): + """Generate a TOTP code. + + A TOTP code is an extension of HOTP algorithm. + + :param period: A period that a TOTP code is valid in seconds + :param timestamp: Create TOTP at this given timestamp + """ + return generate_totp(self.secret, period, timestamp) + + def valid_hotp(self, code, last=0, trials=100): + """Valid a HOTP code. + + :param code: A number that is less than 6 characters. + :param last: Guess HOTP code from last + 1 range. + :param trials: Guest HOTP code end at last + trials + 1. + """ + if not valid_code(code): + return False + + code = bytes(int(code)) + for i in range(last + 1, last + trials + 1): + if compare_digest(bytes(self.hotp(counter=i)), code): + return i + return False + + def valid_totp(self, code, period=30, timestamp=None): + """Valid a TOTP code. + + :param code: A number that is less than 6 characters. + :param period: A period that a TOTP code is valid in seconds + :param timestamp: Validate TOTP at this given timestamp + """ + if not valid_code(code): + return False + return compare_digest( + bytes(self.totp(period, timestamp)), + bytes(int(code)) + ) + + @property + def encoded_secret(self): + secret = base64.b32encode(to_bytes(self.secret)) + # bytes to string + secret = secret.decode('utf-8') + # remove pad string + return secret.strip('=') + + def to_uri(self, type, label, issuer, counter=None): + """Generate the otpauth protocal string. + + :param type: Algorithm type, hotp or totp. + :param label: Label of the identifier. + :param issuer: The company, the organization or something else. + :param counter: Counter of the HOTP algorithm. + """ + type = type.lower() + + if type not in ('hotp', 'totp'): + raise ValueError('type must be hotp or totp') + + if type == 'hotp' and not counter: + raise ValueError('HOTP type authentication need counter') + + # https://code.google.com/p/google-authenticator/wiki/KeyUriFormat + url = ('otpauth://%(type)s/%(label)s?secret=%(secret)s' + '&issuer=%(issuer)s') + dct = dict( + type=type, label=label, issuer=issuer, + secret=self.encoded_secret, counter=counter + ) + ret = url % dct + if type == 'hotp': + ret = '%s&counter=%s' % (ret, counter) + return ret + + def to_google(self, type, label, issuer, counter=None): + """Generate the otpauth protocal string for Google Authenticator. + + .. deprecated:: 0.2.0 + Use :func:`to_uri` instead. + """ + warnings.warn('deprecated, use to_uri instead', DeprecationWarning) + return self.to_uri(type, label, issuer, counter) + + +def generate_hotp(secret, counter=4): + """Generate a HOTP code. + + :param secret: A secret token for the authentication. + :param counter: HOTP is a counter based algorithm. + """ + # https://tools.ietf.org/html/rfc4226 + msg = struct.pack('>Q', counter) + digest = hmac.new(to_bytes(secret), msg, hashlib.sha1).digest() + + ob = digest[19] + if PY2: + ob = ord(ob) + + pos = ob & 15 + base = struct.unpack('>I', digest[pos:pos + 4])[0] & 0x7fffffff + token = base % 1000000 + return token + + +def generate_totp(secret, period=30, timestamp=None): + """Generate a TOTP code. + + A TOTP code is an extension of HOTP algorithm. + + :param secret: A secret token for the authentication. + :param period: A period that a TOTP code is valid in seconds + :param timestamp: Current time stamp. + """ + if timestamp is None: + timestamp = time.time() + counter = int(timestamp) // period + return generate_hotp(secret, counter) + + +def to_bytes(text): + if isinstance(text, string_type): + # Python3 str -> bytes + # Python2 unicode -> str + text = text.encode('utf-8') + return text + + +def valid_code(code): + code = string_type(code) + return code.isdigit() and len(code) <= 6 + + +def compare_digest(a, b): + func = getattr(hmac, 'compare_digest', None) + if func: + return func(a, b) + + # fallback + if len(a) != len(b): + return False + + rv = 0 + if PY2: + from itertools import izip + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + else: + for x, y in zip(a, b): + rv |= x ^ y + return rv == 0 \ No newline at end of file diff --git a/utils/shortcuts.py b/utils/shortcuts.py index e3af916..2f0e1c6 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -2,6 +2,7 @@ import hashlib import time import random +import logging from django.shortcuts import render from django.core.paginator import Paginator @@ -9,6 +10,9 @@ from django.core.paginator import Paginator from rest_framework.response import Response +logger = logging.getLogger("app_info") + + def error_page(request, error_reason): return render(request, "utils/error.html", {"error": error_reason}) @@ -96,7 +100,8 @@ def paginate_data(request, query_set, object_serializer): def paginate(request, query_set, object_serializer=None): try: data= paginate_data(request, query_set, object_serializer) - except Exception: + except Exception as e: + logger.error(str(e)) return error_response(u"参数错误") return success_response(data)