From 0e9996cb17950d4fbd0840b68918c48a02ee3c53 Mon Sep 17 00:00:00 2001 From: sxw Date: Sun, 13 Dec 2015 14:22:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B0=8F=E7=BB=84?= =?UTF-8?q?=E9=82=80=E8=AF=B7=E8=B5=9B=E7=9A=84=E5=AF=86=E7=A0=81=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E6=84=9F=E8=A7=89?= =?UTF-8?q?=E4=B9=8B=E5=89=8D=E6=B5=8B=E8=AF=95=E8=BF=87=E4=BA=86=E5=91=80?= =?UTF-8?q?=EF=BC=8C=E7=BB=93=E6=9E=9C=E8=BF=98=E6=98=AF=E6=9C=89=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E5=9B=A0=E4=B8=BA=E9=AA=8C=E8=AF=81=E5=AF=86?= =?UTF-8?q?=E7=A0=81api=E9=87=8C=E9=80=89=E6=8B=A9=E6=AF=94=E8=B5=9B?= =?UTF-8?q?=E6=97=B6=E9=99=90=E5=AE=9A=E6=AF=94=E8=B5=9B=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=B8=AD=E5=8F=AA=E6=9C=89=E5=B8=A6=E5=AF=86=E7=A0=81=E7=9A=84?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E8=B5=9B=EF=BC=8C=E6=89=80=E4=BB=A5=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E9=AA=8C=E8=AF=81=E5=B8=A6=E5=AF=86=E7=A0=81=E7=9A=84?= =?UTF-8?q?=E5=B0=8F=E7=BB=84=E8=B5=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contest/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/views.py b/contest/views.py index cfc2049b..d1787028 100644 --- a/contest/views.py +++ b/contest/views.py @@ -308,7 +308,7 @@ class ContestPasswordVerifyAPIView(APIView): if serializer.is_valid(): data = request.data try: - contest = Contest.objects.get(id=data["contest_id"], contest_type=PASSWORD_PROTECTED_CONTEST) + contest = Contest.objects.get(id=data["contest_id"], contest_type__in=[PASSWORD_PROTECTED_CONTEST,PASSWORD_PROTECTED_GROUP_CONTEST]) except Contest.DoesNotExist: return error_response(u"比赛不存在") From a9d3a5f1085a43456a5c105ec131c568c42e0882 Mon Sep 17 00:00:00 2001 From: sxw Date: Sun, 13 Dec 2015 14:23:09 +0800 Subject: [PATCH 2/3] =?UTF-8?q?jquery=E9=80=89=E6=8B=A9=E5=99=A8=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AD=97=E7=AC=A6=E4=B8=B2=E9=87=8C=E5=A4=9A=E6=89=93?= =?UTF-8?q?=E4=BA=86=E4=B8=80=E4=B8=AA=E7=A9=BA=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/src/js/app/oj/account/changePassword.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/src/js/app/oj/account/changePassword.js b/static/src/js/app/oj/account/changePassword.js index fe7d908a..e825285a 100644 --- a/static/src/js/app/oj/account/changePassword.js +++ b/static/src/js/app/oj/account/changePassword.js @@ -10,7 +10,7 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c $('form').validator().on('submit', function (e) { e.preventDefault(); - var newPassword = $("#new_password ").val(); + var newPassword = $("#new_password").val(); var password = $("#password").val(); var captcha = $("#captcha").val(); $.ajax({ From c08ec7a2dc084cd75cfe2f3327d6a044307e4f21 Mon Sep 17 00:00:00 2001 From: virusdefender <1670873886@qq.com> Date: Wed, 23 Dec 2015 00:31:43 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20admin=20=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=EF=BC=8Cip=20=E5=A4=AA=E5=AE=BD=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit monitor 不再使用,配置判题服务器的代码移入 judge_dispatcher 里面 添加前端后台判题服务器管理页面一些校验的功能 去掉判题服务器监控的前端和后端 修复比赛 first ac 显示错误的问题 修复两步验证中的错误 tfa 显示 url 增加 qrcode 依赖 完成两步验证的逻辑 fix error package name and add pip mirrorwq 废弃 huey,多数据库连接的时候存在 connection 无法释放的问题,回到 celery 修复 huey 队列不会释放数据库连接的问题,是用法不对 增加关闭两步验证的 api 增加两步验证基础代码 完善 sso 登录部分 规范配置文件写法;数据库用户名也在环境变量中取 个人博客链接前面也增加图标 修改判题机器的配置文件 删除不再使用的配置文件 Squash from a1fff74 to 12f96c6 by virusdefender --- .gitignore | 2 +- account/migrations/0016_auto_20151211_2230.py | 25 +++ account/migrations/0017_auto_20151212_2139.py | 20 ++ account/models.py | 3 + account/serializers.py | 6 +- account/tasks.py | 8 + account/views.py | 119 ++++++++-- contest/models.py | 5 +- db1.sqlite3 | 0 dockerfiles/oj_web_server/Dockerfile | 4 +- dockerfiles/oj_web_server/requirements.txt | 4 +- dockerfiles/oj_web_server/task_queue.conf | 4 +- .../migrations/0003_auto_20151223_0029.py | 25 +++ judge_dispatcher/models.py | 2 + judge_dispatcher/serializers.py | 29 +++ judge_dispatcher/tasks.py | 13 +- judge_dispatcher/views.py | 71 ++++++ monitor/__init__.py | 0 monitor/views.py | 19 -- oj/__init__.py | 5 + oj/celery.py | 19 ++ oj/local_settings.py | 6 + oj/server_settings.py | 6 + oj/settings.py | 16 +- oj/urls.py | 29 +-- static/src/css/oj.css | 10 + static/src/js/app/admin/admin.js | 95 ++++---- static/src/js/app/admin/judges/judges.js | 143 ++++++++++++ static/src/js/app/admin/monitor/monitor.js | 48 ---- static/src/js/app/oj/account/login.js | 23 +- static/src/js/app/oj/account/twoFactorAuth.js | 27 +++ static/src/js/build.js | 100 ++++----- static/src/js/config.js | 50 ++--- submission/tasks.py | 10 +- submission/views.py | 6 +- template/src/admin/judges/judges.html | 131 +++++++++++ template/src/oj/account/avatar.html | 1 + template/src/oj/account/change_password.html | 1 + template/src/oj/account/login.html | 13 +- template/src/oj/account/settings.html | 1 + template/src/oj/account/two_factor_auth.html | 41 ++++ utils/otp_auth.py | 208 ++++++++++++++++++ utils/shortcuts.py | 7 +- 43 files changed, 1078 insertions(+), 277 deletions(-) create mode 100644 account/migrations/0016_auto_20151211_2230.py create mode 100644 account/migrations/0017_auto_20151212_2139.py create mode 100644 account/tasks.py delete mode 100644 db1.sqlite3 create mode 100644 judge_dispatcher/migrations/0003_auto_20151223_0029.py create mode 100644 judge_dispatcher/serializers.py create mode 100644 judge_dispatcher/views.py delete mode 100644 monitor/__init__.py delete mode 100644 monitor/views.py create mode 100644 oj/celery.py create mode 100644 static/src/js/app/admin/judges/judges.js delete mode 100644 static/src/js/app/admin/monitor/monitor.js create mode 100644 static/src/js/app/oj/account/twoFactorAuth.js create mode 100644 template/src/admin/judges/judges.html create mode 100644 template/src/oj/account/two_factor_auth.html create mode 100644 utils/otp_auth.py diff --git a/.gitignore b/.gitignore index e0873616..8707498f 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 00000000..9c2f8798 --- /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 00000000..a34d2fa8 --- /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 b99c6917..65d87450 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 0d92ae7e..a2d27683 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 00000000..58cb8fc1 --- /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 5ca7612a..9c41b349 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 591b54af..fc863ff9 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 e69de29b..00000000 diff --git a/dockerfiles/oj_web_server/Dockerfile b/dockerfiles/oj_web_server/Dockerfile index 07a73e9f..93623f75 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 56676a92..32260e96 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 39f837e0..5a9b13f7 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 00000000..fe2e13af --- /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 7ec5af8f..3bac9474 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 00000000..4328eee9 --- /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 244c9074..51811b10 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 00000000..75f09990 --- /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 e69de29b..00000000 diff --git a/monitor/views.py b/monitor/views.py deleted file mode 100644 index f502579f..00000000 --- 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 f4206374..44aca8db 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 00000000..f20f9dc8 --- /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 794a7b63..8fa3b8ac 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 afec6d19..98eae980 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 f5857033..96c27953 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 77a92b66..fea74415 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 52701b3d..a04f0a0e 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 2c527de7..ec75de13 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 00000000..6e651f23 --- /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 1a0e46f1..00000000 --- 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 8e1351d9..3e35fd05 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 00000000..7c89e5b5 --- /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 8c8de627..df5e2343 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 ee6f52b2..531d349e 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 25476e36..4267841a 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 ccda8acb..41f5e3b5 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 00000000..11ef8e91 --- /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 d551c4a9..25a6d17f 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 9fd13340..25faf1d8 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 eadb5cc0..04302f17 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 53560240..e6436bb5 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 00000000..fe35d74d --- /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 00000000..12773c4e --- /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 e3af9160..2f0e1c65 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)