修改 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:
virusdefender 2015-12-23 00:31:43 +08:00
parent 0e222a6109
commit c08ec7a2dc
43 changed files with 1078 additions and 277 deletions

2
.gitignore vendored
View File

@ -52,7 +52,7 @@ db.db
#redis dump
*.rdb
#*.out
db.sqlite3
*.sqlite3
.DS_Store
log/
static/release/css

View 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),
),
]

View 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),
),
]

View File

@ -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 = []

View File

@ -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
View 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)

View File

@ -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)

View File

@ -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:

View File

View File

@ -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

View File

@ -11,4 +11,6 @@ supervisor
pillow
jsonfield
Envelopes
huey
celery
django-celery
qrcode

View File

@ -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

View 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,
),
]

View File

@ -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否则取到的是旧数据

View 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

View File

@ -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
View 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)

View File

View File

@ -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})

View File

@ -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
View 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)

View File

@ -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 = []

View File

@ -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 = ['*']

View File

@ -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",

View File

@ -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')))

View File

@ -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;
}

View File

@ -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("请求失败");
}
});

View 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();
});

View File

@ -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);
});

View File

@ -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();
});
});

View 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();
}
}
})
})
});

View File

@ -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",

View File

@ -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: {

View File

@ -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()

View File

@ -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"提交判题任务失败")

View 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>
&nbsp;&nbsp;
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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>&nbsp;&nbsp;<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">

View File

@ -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>

View 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
View 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

View File

@ -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)