mirror of
https://github.com/QingdaoU/OnlineJudge.git
synced 2025-09-26 23:09:21 +08:00
修改 admin 界面,ip 太宽了
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
This commit is contained in:
parent
0e222a6109
commit
c08ec7a2dc
2
.gitignore
vendored
2
.gitignore
vendored
@ -52,7 +52,7 @@ db.db
|
||||
#redis dump
|
||||
*.rdb
|
||||
#*.out
|
||||
db.sqlite3
|
||||
*.sqlite3
|
||||
.DS_Store
|
||||
log/
|
||||
static/release/css
|
||||
|
25
account/migrations/0016_auto_20151211_2230.py
Normal file
25
account/migrations/0016_auto_20151211_2230.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
20
account/migrations/0017_auto_20151212_2139.py
Normal file
20
account/migrations/0017_auto_20151212_2139.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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 = []
|
||||
|
@ -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()
|
||||
|
8
account/tasks.py
Normal file
8
account/tasks.py
Normal file
@ -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)
|
119
account/views.py
119
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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
CMD supervisord
|
||||
|
@ -11,4 +11,6 @@ supervisor
|
||||
pillow
|
||||
jsonfield
|
||||
Envelopes
|
||||
huey
|
||||
celery
|
||||
django-celery
|
||||
qrcode
|
||||
|
@ -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
|
||||
|
25
judge_dispatcher/migrations/0003_auto_20151223_0029.py
Normal file
25
judge_dispatcher/migrations/0003_auto_20151223_0029.py
Normal file
@ -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,
|
||||
),
|
||||
]
|
@ -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,否则取到的是旧数据
|
||||
|
29
judge_dispatcher/serializers.py
Normal file
29
judge_dispatcher/serializers.py
Normal file
@ -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
|
@ -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()
|
||||
|
||||
|
71
judge_dispatcher/views.py
Normal file
71
judge_dispatcher/views.py
Normal file
@ -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)
|
@ -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})
|
@ -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
|
19
oj/celery.py
Normal file
19
oj/celery.py
Normal file
@ -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)
|
@ -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 = []
|
||||
|
@ -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 = ['*']
|
||||
|
@ -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"青岛大学信息工程学院 创新实验室 <a href=\"http://www.miibeian.gov.cn/\">京ICP备15062075号-1</a>",
|
||||
"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",
|
||||
|
29
oj/urls.py
29
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<contest_id>\d+)/problem/(?P<contest_problem_id>\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<page>\d+)/$', "contest.views.contest_list_page", name="contest_list_page"),
|
||||
|
||||
|
||||
url(r'^problem/(?P<problem_id>\d+)/$', "problem.views.problem_page", name="problem_page"),
|
||||
url(r'^problems/$', "problem.views.problem_list_page", name="problem_list_page"),
|
||||
url(r'^problems/(?P<page>\d+)/$', "problem.views.problem_list_page", name="problem_list_page"),
|
||||
url(r'^problem/(?P<problem_id>\d+)/submissions/$', "submission.views.problem_my_submissions_list_page",
|
||||
name="problem_my_submissions_page"),
|
||||
|
||||
|
||||
url(r'^submission/(?P<submission_id>\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<page>\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<token>\w+)/$', "account.views.reset_password_page", name="reset_password_page")
|
||||
url(r'^reset_password/t/(?P<token>\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')))
|
||||
urlpatterns.append(url(r'^docs/', include('rest_framework_swagger.urls')))
|
||||
|
@ -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;
|
||||
}
|
@ -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("请求失败");
|
||||
}
|
||||
});
|
||||
|
143
static/src/js/app/admin/judges/judges.js
Normal file
143
static/src/js/app/admin/judges/judges.js
Normal file
@ -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();
|
||||
});
|
@ -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);
|
||||
|
||||
});
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
27
static/src/js/app/oj/account/twoFactorAuth.js
Normal file
27
static/src/js/app/oj/account/twoFactorAuth.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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: {
|
||||
|
@ -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)
|
||||
@shared_task
|
||||
def _judge(submission, time_limit, memory_limit, test_case_id):
|
||||
JudgeDispatcher(submission, time_limit, memory_limit, test_case_id).judge()
|
@ -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"提交判题任务失败")
|
||||
|
131
template/src/admin/judges/judges.html
Normal file
131
template/src/admin/judges/judges.html
Normal file
@ -0,0 +1,131 @@
|
||||
<div ms-controller="judges" class="col-md-9">
|
||||
<h1>判题服务器管理</h1>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>编号</th>
|
||||
<th>名字</th>
|
||||
<th>最大实例数量</th>
|
||||
<th>负载</th>
|
||||
<th>创建时间</th>
|
||||
<th>状态</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ms-repeat="judgesList">
|
||||
<td>{{ el.id }}</td>
|
||||
<td>{{ el.name }}</td>
|
||||
<td>{{ el.max_instance_number }}</td>
|
||||
<td>{{ el.workload }}</td>
|
||||
<td>{{ el.create_time|date("yyyy-MM-dd HH:mm:ss")}}</td>
|
||||
<td ms-text="el.status?'启用':'停用'"></td>
|
||||
<td>
|
||||
<button class="btn-sm btn-info" ms-click="editJudges(el)">编辑</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="form-group">
|
||||
<label>仅显示启用 <input ms-duplex-checked="showEnableOnly" type="checkbox"/></label>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ms:pager $id="judgesPager" config="pager"></ms:pager>
|
||||
</div>
|
||||
|
||||
<div ms-visible="isEditing">
|
||||
<h3>编辑判题服务器</h3>
|
||||
|
||||
<form id="edit-judges-form">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>名字</label>
|
||||
<input name="title" type="text" class="form-control" placeholder="名字" maxlength="30" ms-duplex="name" required data-error="请填写合法的服务器名称">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>口令</label>
|
||||
<input name="title" type="text" class="form-control" ms-duplex="token" placeholder="口令" maxlength="30" required data-error="请填写合法的口令">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>IP</label>
|
||||
<input name="ip" type="text" class="form-control" ms-duplex="ipAddress" placeholder="IP" required data-error="请填写合法的IP地址">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>端口</label>
|
||||
<input name="port" type="number" class="form-control" ms-duplex="port" placeholder="端口" required data-error="请填写合法的端口号">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>最大实例数量</label>
|
||||
<input type="number" class="form-control" placeholder="最大实例数量" ms-duplex="max_instance_number" required data-error="请填写合法的最大实例数量">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>启用 <input ms-duplex-checked="status" type="checkbox"/></label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-success">保存修改</button>
|
||||
|
||||
<a ms-click="cancelEdit()" class="btn btn-danger">取消</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h3>添加判题服务器</h3>
|
||||
|
||||
<form id="judges-form">
|
||||
<div class="form-group">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>名字</label>
|
||||
<input name="title" type="text" class="form-control" id="name" placeholder="名字" maxlength="30" required data-error="请填写合法的服务器名称">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>口令</label>
|
||||
<input name="title" type="text" class="form-control" id="token" placeholder="口令" maxlength="30" required data-error="请填写合法的口令">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>IP</label>
|
||||
<input name="ip" type="text" class="form-control" id="ipAddress" placeholder="IP" required data-error="请填写合法的IP地址">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>端口</label>
|
||||
<input name="port" type="number" class="form-control" id="port" placeholder="端口" required data-error="请填写合法的端口号">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>最大实例数量</label>
|
||||
<input type="number" class="form-control" placeholder="最大实例数量" id="max_instance_number" required data-error="请填写合法的最大实例数量">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-success">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="/static/js/app/admin/judges/judges.js"></script>
|
@ -10,6 +10,7 @@
|
||||
<li class="list-group-header">通用设置</li>
|
||||
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
||||
<li class="list-group-item active"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
|
||||
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<li class="list-group-header">通用设置</li>
|
||||
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
||||
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
|
||||
<li class="list-group-item active"><a href="/change_password/">修改密码</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -11,22 +11,21 @@
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" class="form-control input-lg" id="username" name="username" maxlength="30"
|
||||
data-error="请填写用户名" placeholder="用户名" autofocus required>
|
||||
data-error="请填写用户名" placeholder="用户名" autofocus required autocomplete="off">
|
||||
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" class="form-control input-lg" id="password" name="password" maxlength="30"
|
||||
data-error="请填写密码" placeholder="密码" required>
|
||||
data-error="请填写密码" placeholder="密码" required autocomplete="off">
|
||||
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
<div class="form-group" id="captcha-area">
|
||||
<label for="captcha">验证码</label> <img src="/captcha/" id="captcha-img"><small>
|
||||
<p></p></small>
|
||||
<input type="text" class="form-control input-lg" id="captcha" name="captcha"
|
||||
placeholder="验证码" maxlength="4" data-error="请填写验证码" required>
|
||||
<div class="form-group" id="tfa-area">
|
||||
<label for="captcha">两步验证</label>
|
||||
<input type="text" class="form-control input-lg" id="tfa-code" name="tfa-code"
|
||||
placeholder="两步验证验证码" maxlength="6" data-error="请填写两步验证验证码" required autocomplete="off">
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -10,6 +10,7 @@
|
||||
<li class="list-group-header">通用设置</li>
|
||||
<li class="list-group-item active"><a href="/account/settings/">个人信息</a></li>
|
||||
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
|
||||
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
41
template/src/oj/account/two_factor_auth.html
Normal file
41
template/src/oj/account/two_factor_auth.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% extends "oj_base.html" %}
|
||||
{% block title %}
|
||||
两步验证
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="container main">
|
||||
|
||||
<div class="col-lg-2">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-header">通用设置</li>
|
||||
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
||||
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||
<li class="list-group-item active"><a href="/two_factor_auth/">两步验证</a></li>
|
||||
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
{% if not request.user.two_factor_auth %}
|
||||
<h3>扫描二维码开启两步验证</h3>
|
||||
<img src="/api/two_factor_auth/" id="tfa-qrcode">
|
||||
|
||||
<div class="form-inline">
|
||||
<div class="form-group">
|
||||
<label for="tfa_code">验证码</label>
|
||||
<input type="text" maxlength="6" class="form-control" id="tfa_code"
|
||||
placeholder="输入 app 上显示的验证码">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="tfa_submit">确定</button>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-success" role="alert">两步验证已经开启</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_block %}
|
||||
<script src="/static/js/app/oj/account/twoFactorAuth.js"></script>
|
||||
{% endblock %}
|
208
utils/otp_auth.py
Normal file
208
utils/otp_auth.py
Normal file
@ -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 <me@lepture.com>'
|
||||
__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
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user